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
This commit is contained in:
Matt Mascarenhas 2023-01-11 19:24:41 +00:00
parent 554f7393ff
commit 6b09247cd2
4 changed files with 108 additions and 19 deletions

View File

@ -23,7 +23,7 @@ typedef struct
version CINERA_APP_VERSION = { version CINERA_APP_VERSION = {
.Major = 0, .Major = 0,
.Minor = 10, .Minor = 10,
.Patch = 19 .Patch = 20
}; };
#define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW #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." }, 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." }, { "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." }, { "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." }, { "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", "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." }, { "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_CREDIT,
IDENT_CSS_PATH, IDENT_CSS_PATH,
IDENT_DB_LOCATION, IDENT_DB_LOCATION,
IDENT_DEFAULT_CC_LANG,
IDENT_DEFAULT_MEDIUM, IDENT_DEFAULT_MEDIUM,
IDENT_DENY, IDENT_DENY,
IDENT_DENY_BESPOKE_TEMPLATES, IDENT_DENY_BESPOKE_TEMPLATES,
@ -1963,18 +1965,14 @@ ConfigError(string *Filename, uint64_t LineNumber, severity Severity, char *Mess
void void
ConfigErrorField(string *Filename, uint64_t LineNumber, severity Severity, config_identifier_id FieldID, 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); ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_CONFIG);
fprintf(stderr, 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_YELLOW_BOLD], ConfigIdentifiers[FieldID].String, ColourStrings[CS_END],
ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END],
Message); Message);
if(Received)
{
PrintStringC(CS_MAGENTA_BOLD, *Received);
}
fprintf(stderr, "\n");
} }
void void
@ -2139,6 +2137,25 @@ IndexingErrorClash(string *Filename, uint64_t LineNumber, severity Severity, cha
fprintf(stderr, "\n"); 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 void
IndexingErrorInvalidSubstring(string *Filename, uint64_t LineNumber, char *Key, string Received, string InvalidSubstring) 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 == '-'); 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 bool
IsNumber(char C) 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 = {}; string OutputLocation = {};
if(!HMML.metadata.output) if(!HMML.metadata.output)
{ {
@ -11230,8 +11276,17 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF
CopyStringToBuffer(&PlayerBuffers.Main, CopyStringToBuffer(&PlayerBuffers.Main,
"<div class=\"cineraPlayerContainer\">\n" "<div class=\"cineraPlayerContainer\">\n"
" <div class=\"video_container\" data-platform=\"%s\" data-videoId=\"%s\"></div>\n" " <div class=\"video_container\" data-platform=\"%s\" data-videoId=\"%s\"", VODPlatformStrings[CollationBuffers->VODPlatform], HMML.metadata.id);
" <div class=\"markers_container %.*s\">\n", VODPlatformStrings[CollationBuffers->VODPlatform], HMML.metadata.id, (int)CurrentProject->Theme.Length, CurrentProject->Theme.Base);
if(CCLang.Length > 0)
{
CopyStringToBuffer(&PlayerBuffers.Main,
" data-ccLang=\"%.*s\"", (int)CCLang.Length, CCLang.Base);
}
CopyStringToBuffer(&PlayerBuffers.Main,
"></div>\n"
" <div class=\"markers_container %.*s\">\n", (int)CurrentProject->Theme.Length, CurrentProject->Theme.Base);
if(N) if(N)
{ {

View File

@ -526,6 +526,7 @@ typedef struct project
string TemplatesDir; string TemplatesDir;
string SearchTemplatePath; string SearchTemplatePath;
string PlayerTemplatePath; string PlayerTemplatePath;
string DefaultCCLang;
string BaseDir; string BaseDir;
string BaseURL; string BaseURL;
@ -918,6 +919,7 @@ InitTypeSpecs(void)
PushTypeSpecField(Root, FT_STRING, IDENT_COHOST, FALSE); PushTypeSpecField(Root, FT_STRING, IDENT_COHOST, FALSE);
PushTypeSpecField(Root, FT_STRING, IDENT_CSS_PATH, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_CSS_PATH, TRUE);
PushTypeSpecField(Root, FT_STRING, IDENT_DB_LOCATION, 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_DEFAULT_MEDIUM, TRUE);
PushTypeSpecField(Root, FT_STRING, IDENT_GENRE, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_GENRE, TRUE);
PushTypeSpecField(Root, FT_STRING, IDENT_GLOBAL_SEARCH_DIR, 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); config_type_spec *Project = PushTypeSpec(&Result, IDENT_PROJECT, TRUE);
PushTypeSpecField(Project, FT_STRING, IDENT_BASE_DIR, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_BASE_DIR, TRUE);
PushTypeSpecField(Project, FT_STRING, IDENT_BASE_URL, 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_DEFAULT_MEDIUM, TRUE);
PushTypeSpecField(Project, FT_STRING, IDENT_GENRE, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_GENRE, TRUE);
PushTypeSpecField(Project, FT_STRING, IDENT_HMML_DIR, 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) if(QuotemarkCount == 1)
{ {
string Filepath = Wrap0(Pair->Position.Filename); string Filepath = Wrap0(Pair->Position.Filename);
ConfigErrorField(&Filepath, Pair->Position.LineNumber, S_ERROR, IDENT_NAME, ConfigErrorField(&Filepath, Pair->Position.LineNumber, S_ERROR, IDENT_NAME, &This->Name,
"contains unpaired quotation mark: ", &This->Name); "contains unpaired quotation mark");
PushError(E, S_ERROR, 0, Pair->Key); PushError(E, S_ERROR, 0, Pair->Key);
NamingError = TRUE; NamingError = TRUE;
} }
else if(QuotemarkCount > 2) else if(QuotemarkCount > 2)
{ {
string Filepath = Wrap0(Pair->Position.Filename); string Filepath = Wrap0(Pair->Position.Filename);
ConfigErrorField(&Filepath, Pair->Position.LineNumber, S_ERROR, IDENT_NAME, ConfigErrorField(&Filepath, Pair->Position.LineNumber, S_ERROR, IDENT_NAME, &This->Name,
"contains more than one pair of quotation marks: ", &This->Name); "contains more than one pair of quotation marks");
PushError(E, S_ERROR, 0, Pair->Key); PushError(E, S_ERROR, 0, Pair->Key);
NamingError = TRUE; NamingError = TRUE;
} }
@ -3744,6 +3747,17 @@ PushProject(config *C, resolution_errors *E, config_verifiers *V, project *P, sc
switch(This->Key) switch(This->Key)
{ {
// NOTE(matt): String // 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: case IDENT_DEFAULT_MEDIUM:
{ P->DefaultMedium = GetMediumFromProject(P, This->String); } break; { P->DefaultMedium = GetMediumFromProject(P, This->String); } break;
case IDENT_HMML_DIR: 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_TITLE, P->Title, AvailableColumns);
TypesetPair(T, Generation, IDENT_HTML_TITLE, P->HTMLTitle, 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_DEFAULT_MEDIUM, P->DefaultMedium ? P->DefaultMedium->ID : EmptyString(), AvailableColumns);
TypesetPair(T, Generation, IDENT_HMML_DIR, P->HMMLDir, AvailableColumns); TypesetPair(T, Generation, IDENT_HMML_DIR, P->HMMLDir, AvailableColumns);

View File

@ -965,10 +965,19 @@ Player.prototype.onPlatformReady = function() {
{ {
this.videoContainer.style.position = "relative"; this.videoContainer.style.position = "relative";
this.videoContainer.style.alignSelf = "unset"; this.videoContainer.style.alignSelf = "unset";
this.platformPlayer = new Vimeo.Player(platformPlayerDiv.id, {
var CallData = {
id: this.videoContainer.getAttribute("data-videoId"), id: this.videoContainer.getAttribute("data-videoId"),
title: false, 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() this.platformPlayer.ready()
.then(this.onPlatformPlayerReady.bind(this)); .then(this.onPlatformPlayerReady.bind(this));
this.platformPlayer.on("playbackratechange", this.onVimeoPlayerPlaybackRateChange.bind(this)); this.platformPlayer.on("playbackratechange", this.onVimeoPlayerPlaybackRateChange.bind(this));
@ -980,7 +989,7 @@ Player.prototype.onPlatformReady = function() {
} break; } break;
case vod_platform.YOUTUBE: case vod_platform.YOUTUBE:
{ {
this.platformPlayer = new YT.Player(platformPlayerDiv.id, { var CallData = {
videoId: this.videoContainer.getAttribute("data-videoId"), videoId: this.videoContainer.getAttribute("data-videoId"),
width: this.videoContainer.offsetWidth, width: this.videoContainer.offsetWidth,
height: this.videoContainer.offsetWidth / 16 * 9, height: this.videoContainer.offsetWidth / 16 * 9,
@ -990,7 +999,15 @@ Player.prototype.onPlatformReady = function() {
"onStateChange": this.onYouTubePlayerStateChange.bind(this), "onStateChange": this.onYouTubePlayerStateChange.bind(this),
"onPlaybackRateChange": this.onYouTubePlayerPlaybackRateChange.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; } break;
} }
}; };

View File

@ -25,6 +25,7 @@ typedef struct {
char* template; char* template;
char* medium; char* medium;
char* number; char* number;
char* cc_lang;
HMML_Credit* credits; HMML_Credit* credits;
size_t credit_count; size_t credit_count;
@ -737,6 +738,7 @@ static void _hmml_parse_video(struct _hmml_parser* p)
{ HSTR("medium") , &p->out.metadata.medium }, { HSTR("medium") , &p->out.metadata.medium },
{ HSTR("number") , &p->out.metadata.number }, { HSTR("number") , &p->out.metadata.number },
{ HSTR("output") , &p->out.metadata.output }, { HSTR("output") , &p->out.metadata.output },
{ HSTR("cc_lang") , &p->out.metadata.cc_lang },
}; };
for(;;) { for(;;) {
@ -822,7 +824,7 @@ void hmml_free(HMML_Output* out)
} }
const struct HMML_Version hmml_version = { const struct HMML_Version hmml_version = {
2, 0, 13 2, 0, 14
}; };
#undef HSTX #undef HSTX