From 6b09247cd2acf54f05298545f6e429bb3d02952d Mon Sep 17 00:00:00 2001 From: Matt Mascarenhas Date: Wed, 11 Jan 2023 19:24:41 +0000 Subject: [PATCH] cinera.c: Enable default closed captions This commit adds support for configurably enabling closed captions by default. Set the "default_cc_lang" in a config file, or "cc_lang" in the [video] node of an .hmml file. Once set, videos which contain captions for this langage will be initialised with these captions enabled. New config setting: default_cc_lang New [video] node field: cc_lang --- cinera/cinera.c | 75 ++++++++++++++++++++++++++++++++----- cinera/cinera_config.c | 23 ++++++++++-- cinera/cinera_player_pre.js | 25 +++++++++++-- hmmlib2/hmmlib.h | 4 +- 4 files changed, 108 insertions(+), 19 deletions(-) diff --git a/cinera/cinera.c b/cinera/cinera.c index d45ba56..c3e9530 100644 --- a/cinera/cinera.c +++ b/cinera/cinera.c @@ -23,7 +23,7 @@ typedef struct version CINERA_APP_VERSION = { .Major = 0, .Minor = 10, - .Patch = 19 + .Patch = 20 }; #define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW @@ -1451,6 +1451,7 @@ file credits this person for the entire project, but indexers may \"uncredit\" p file credits this person for the entire project, but indexers may \"uncredit\" people in the [video] node of a HMML file." }, { "css_path", "Path relative to assets_root_dir and assets_root_url where CSS files are located." }, { "db_location", "Absolute file path where the database file resides. If you run multiple instances of Cinera on the same machine, please ensure this db_location differs between them." }, + { "default_cc_lang", "The ISO 639-1 language code of the closed captions to enable by default. May be overridden by setting the cc_lang in the video node of an HMML file." }, { "default_medium", "The ID of a medium (see also medium) which will be the default for the project. May be overridden by setting the medium in the video node of an HMML file." }, { "deny", "See allow." }, { "deny_bespoke_templates", "Indexers may use the \"template\" attribute in the video node of an .hmml file to set a bespoke template for that entry, superseding any configured player_template. Setting \"deny_bespoke_templates\" to true prevents this." }, @@ -1586,6 +1587,7 @@ typedef enum IDENT_CREDIT, IDENT_CSS_PATH, IDENT_DB_LOCATION, + IDENT_DEFAULT_CC_LANG, IDENT_DEFAULT_MEDIUM, IDENT_DENY, IDENT_DENY_BESPOKE_TEMPLATES, @@ -1963,18 +1965,14 @@ ConfigError(string *Filename, uint64_t LineNumber, severity Severity, char *Mess void ConfigErrorField(string *Filename, uint64_t LineNumber, severity Severity, config_identifier_id FieldID, - char *Message, string *Received) + string *Received, char *Message) { ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_CONFIG); fprintf(stderr, - "Faulty %s%s%s %s", + "%s%s%s value %s%.*s%s %s\n", ColourStrings[CS_YELLOW_BOLD], ConfigIdentifiers[FieldID].String, ColourStrings[CS_END], + ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END], Message); - if(Received) - { - PrintStringC(CS_MAGENTA_BOLD, *Received); - } - fprintf(stderr, "\n"); } void @@ -2139,6 +2137,25 @@ IndexingErrorClash(string *Filename, uint64_t LineNumber, severity Severity, cha fprintf(stderr, "\n"); } +void +PrintValidLanguageCodeChars(void) +{ + fprintf(stderr, + " Valid characters:\n" + " a to z\n" + " A to Z\n" + " 0 to 9\n" + " - (hyphen)\n"); +} + +void +IndexingErrorInvalidLanguageCode(string *Filename, uint64_t LineNumber, char *Key, string Received) +{ + ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING); + fprintf(stderr, "%s value %s%.*s%s contains invalid character(s)\n", + Key, ColourStrings[CS_MAGENTA_BOLD], (int)Received.Length, Received.Base, ColourStrings[CS_END]); +} + void IndexingErrorInvalidSubstring(string *Filename, uint64_t LineNumber, char *Key, string Received, string InvalidSubstring) { @@ -2481,6 +2498,23 @@ IsValidIdentifierCharacter(char C) return ((C >= 'a' && C <= 'z') || (C >= 'A' && C <= 'Z') || C == '_' || C == '-'); } +bool +IsValidLanguageCode(string S) +{ + // TODO(matt): Add an upper limit to the length + bool Result = TRUE; + for(int i = 0; i < S.Length; ++i) + { + char C = S.Base[i]; + if(!((C >= '0' && C <= '9') || (C >= 'a' && C <= 'z') || (C >= 'A' && C <= 'Z') || C == '-')) + { + Result = FALSE; + break; + } + } + return Result; +} + bool IsNumber(char C) { @@ -11054,6 +11088,18 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF } } + string CCLang = CurrentProject->DefaultCCLang; + if(HMML.metadata.cc_lang) + { + CCLang = Wrap0(HMML.metadata.cc_lang); + if(!IsValidLanguageCode(CCLang)) + { + IndexingErrorInvalidLanguageCode(&FilepathL, 0, "cc_lang", CCLang); + PrintValidLanguageCodeChars(); + Result = RC_ERROR_HMML; + } + } + string OutputLocation = {}; if(!HMML.metadata.output) { @@ -11230,8 +11276,17 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF CopyStringToBuffer(&PlayerBuffers.Main, "
\n" - "
\n" - "
\n", VODPlatformStrings[CollationBuffers->VODPlatform], HMML.metadata.id, (int)CurrentProject->Theme.Length, CurrentProject->Theme.Base); + "
VODPlatform], HMML.metadata.id); + + if(CCLang.Length > 0) + { + CopyStringToBuffer(&PlayerBuffers.Main, + " data-ccLang=\"%.*s\"", (int)CCLang.Length, CCLang.Base); + } + + CopyStringToBuffer(&PlayerBuffers.Main, + ">
\n" + "
\n", (int)CurrentProject->Theme.Length, CurrentProject->Theme.Base); if(N) { diff --git a/cinera/cinera_config.c b/cinera/cinera_config.c index 5936b56..8828196 100644 --- a/cinera/cinera_config.c +++ b/cinera/cinera_config.c @@ -526,6 +526,7 @@ typedef struct project string TemplatesDir; string SearchTemplatePath; string PlayerTemplatePath; + string DefaultCCLang; string BaseDir; string BaseURL; @@ -918,6 +919,7 @@ InitTypeSpecs(void) PushTypeSpecField(Root, FT_STRING, IDENT_COHOST, FALSE); PushTypeSpecField(Root, FT_STRING, IDENT_CSS_PATH, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_DB_LOCATION, TRUE); + PushTypeSpecField(Root, FT_STRING, IDENT_DEFAULT_CC_LANG, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_DEFAULT_MEDIUM, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_GENRE, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_GLOBAL_SEARCH_DIR, TRUE); @@ -1018,6 +1020,7 @@ InitTypeSpecs(void) config_type_spec *Project = PushTypeSpec(&Result, IDENT_PROJECT, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_BASE_DIR, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_BASE_URL, TRUE); + PushTypeSpecField(Project, FT_STRING, IDENT_DEFAULT_CC_LANG, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_DEFAULT_MEDIUM, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_GENRE, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_HMML_DIR, TRUE); @@ -3491,16 +3494,16 @@ PushPersonOntoConfig(config *C, resolution_errors *E, config_verifiers *V, scope if(QuotemarkCount == 1) { string Filepath = Wrap0(Pair->Position.Filename); - ConfigErrorField(&Filepath, Pair->Position.LineNumber, S_ERROR, IDENT_NAME, - "contains unpaired quotation mark: ", &This->Name); + ConfigErrorField(&Filepath, Pair->Position.LineNumber, S_ERROR, IDENT_NAME, &This->Name, + "contains unpaired quotation mark"); PushError(E, S_ERROR, 0, Pair->Key); NamingError = TRUE; } else if(QuotemarkCount > 2) { string Filepath = Wrap0(Pair->Position.Filename); - ConfigErrorField(&Filepath, Pair->Position.LineNumber, S_ERROR, IDENT_NAME, - "contains more than one pair of quotation marks: ", &This->Name); + ConfigErrorField(&Filepath, Pair->Position.LineNumber, S_ERROR, IDENT_NAME, &This->Name, + "contains more than one pair of quotation marks"); PushError(E, S_ERROR, 0, Pair->Key); NamingError = TRUE; } @@ -3744,6 +3747,17 @@ PushProject(config *C, resolution_errors *E, config_verifiers *V, project *P, sc switch(This->Key) { // NOTE(matt): String + case IDENT_DEFAULT_CC_LANG: + { + P->DefaultCCLang = ResolveString(C, E, ProjectTree, This, FALSE); + if(!IsValidLanguageCode(P->DefaultCCLang)) + { + ConfigErrorField(&Filepath, This->Position.LineNumber, S_ERROR, This->Key, &P->DefaultCCLang, + "contains invalid character(s)"); + PrintValidLanguageCodeChars(); + PushError(E, S_ERROR, 0, This->Key); + } + } break; case IDENT_DEFAULT_MEDIUM: { P->DefaultMedium = GetMediumFromProject(P, This->String); } break; case IDENT_HMML_DIR: @@ -4801,6 +4815,7 @@ PrintProject(config *C, project *P, typography *T, int Ancestors, int Indentatio TypesetPair(T, Generation, IDENT_TITLE, P->Title, AvailableColumns); TypesetPair(T, Generation, IDENT_HTML_TITLE, P->HTMLTitle, AvailableColumns); + TypesetPair(T, Generation, IDENT_DEFAULT_CC_LANG, P->DefaultCCLang.Length ? P->DefaultCCLang : EmptyString(), AvailableColumns); TypesetPair(T, Generation, IDENT_DEFAULT_MEDIUM, P->DefaultMedium ? P->DefaultMedium->ID : EmptyString(), AvailableColumns); TypesetPair(T, Generation, IDENT_HMML_DIR, P->HMMLDir, AvailableColumns); diff --git a/cinera/cinera_player_pre.js b/cinera/cinera_player_pre.js index ca66876..98adc52 100644 --- a/cinera/cinera_player_pre.js +++ b/cinera/cinera_player_pre.js @@ -965,10 +965,19 @@ Player.prototype.onPlatformReady = function() { { this.videoContainer.style.position = "relative"; this.videoContainer.style.alignSelf = "unset"; - this.platformPlayer = new Vimeo.Player(platformPlayerDiv.id, { + + var CallData = { id: this.videoContainer.getAttribute("data-videoId"), title: false, - }); + }; + var CCLang = this.videoContainer.getAttribute("data-ccLang"); + if(CCLang != null) + { + CallData.texttrack = CCLang; + } + + this.platformPlayer = new Vimeo.Player(platformPlayerDiv.id, CallData); + this.platformPlayer.ready() .then(this.onPlatformPlayerReady.bind(this)); this.platformPlayer.on("playbackratechange", this.onVimeoPlayerPlaybackRateChange.bind(this)); @@ -980,7 +989,7 @@ Player.prototype.onPlatformReady = function() { } break; case vod_platform.YOUTUBE: { - this.platformPlayer = new YT.Player(platformPlayerDiv.id, { + var CallData = { videoId: this.videoContainer.getAttribute("data-videoId"), width: this.videoContainer.offsetWidth, height: this.videoContainer.offsetWidth / 16 * 9, @@ -990,7 +999,15 @@ Player.prototype.onPlatformReady = function() { "onStateChange": this.onYouTubePlayerStateChange.bind(this), "onPlaybackRateChange": this.onYouTubePlayerPlaybackRateChange.bind(this) } - }); + }; + var CCLang = this.videoContainer.getAttribute("data-ccLang"); + if(CCLang != null) + { + CallData.cc_lang_pref = CCLang; + CallData.cc_load_policy = 1; + } + + this.platformPlayer = new YT.Player(platformPlayerDiv.id, CallData); } break; } }; diff --git a/hmmlib2/hmmlib.h b/hmmlib2/hmmlib.h index d9ce48d..6591f1c 100644 --- a/hmmlib2/hmmlib.h +++ b/hmmlib2/hmmlib.h @@ -25,6 +25,7 @@ typedef struct { char* template; char* medium; char* number; + char* cc_lang; HMML_Credit* credits; size_t credit_count; @@ -737,6 +738,7 @@ static void _hmml_parse_video(struct _hmml_parser* p) { HSTR("medium") , &p->out.metadata.medium }, { HSTR("number") , &p->out.metadata.number }, { HSTR("output") , &p->out.metadata.output }, + { HSTR("cc_lang") , &p->out.metadata.cc_lang }, }; for(;;) { @@ -822,7 +824,7 @@ void hmml_free(HMML_Output* out) } const struct HMML_Version hmml_version = { - 2, 0, 13 + 2, 0, 14 }; #undef HSTX