' Roku translates the info provided in subtitleTracks into availableSubtitleTracks ' Including ignoring tracks, if they are not understood, thus making indexing unpredictable. ' This function translates between our internel selected subtitle index ' and the corresponding index in availableSubtitleTracks. function availSubtitleTrackIdx(video, sub_idx) as integer url = video.Subtitles[sub_idx].Track.TrackName idx = 0 for each availTrack in video.availableSubtitleTracks ' The TrackName must contain the URL we supplied originally, though ' Roku mangles the name a bit, so we check if the URL is a substring, rather ' than strict equality if Instr(1, availTrack.TrackName, url) return idx end if idx = idx + 1 end for return -1 end function ' Identify the default subtitle track for a given video id ' returns the server-side track index for the appriate subtitle function defaultSubtitleTrackFromVid(video_id) as integer meta = ItemMetaData(video_id) if isValid(meta) and isValid(meta.json) and isValid(meta.json.mediaSources) subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams) default_text_subs = defaultSubtitleTrack(subtitles["all"], true) ' Find correct subtitle track (forced text) if default_text_subs <> -1 return default_text_subs else if m.global.session.user.settings["playback.subs.onlytext"] = false return defaultSubtitleTrack(subtitles["all"]) ' if no appropriate text subs exist, allow non-text else return -1 end if end if end if ' No valid mediaSources (i.e. LiveTV) return -1 end function ' Identify the default subtitle track ' if "requires_text" is true, only return a track if it is textual ' This allows forcing text subs, since roku requires transcoding of non-text subs ' returns the server-side track index for the appriate subtitle function defaultSubtitleTrack(sorted_subtitles, require_text = false) as integer if m.global.session.user.configuration.SubtitleMode = "None" return -1 ' No subtitles desired: select none end if for each item in sorted_subtitles ' Only auto-select subtitle if language matches preference languageMatch = (m.global.session.user.configuration.SubtitleLanguagePreference = item.Track.Language) ' Ensure textuality of subtitle matches preferenced passed as arg matchTextReq = ((require_text and item.IsTextSubtitleStream) or not require_text) if languageMatch and matchTextReq if m.global.session.user.configuration.SubtitleMode = "Default" and (item.isForced or item.IsDefault or item.IsExternal) return item.Index ' Finds first forced, or default, or external subs in sorted list else if m.global.session.user.configuration.SubtitleMode = "Always" and not item.IsForced return item.Index ' Select the first non-forced subtitle option in the sorted list else if m.global.session.user.configuration.SubtitleMode = "OnlyForced" and item.IsForced return item.Index ' Select the first forced subtitle option in the sorted list else if m.global.session.user.configuration.SubtitlePlaybackMode = "Smart" and (item.isForced or item.IsDefault or item.IsExternal) ' Simplified "Smart" logic here mimics Default (as that is fallback behavior normally) ' Avoids detecting preferred audio language (as is utilized in main client) return item.Index end if end if end for return -1 ' Keep current default behavior of "None", if no correct subtitle is identified end function ' Given a set of subtitles, and a subtitle index (the index on the server, not in the list provided) ' this will set all relevant settings for roku (mainly closed captions) and return the index of the ' subtitle track specified, but indexed based on the provided list of subtitles function setupSubtitle(video, subtitles, subtitle_idx = -1) as integer if subtitle_idx = -1 ' If we are not using text-based subtitles, turn them off return -1 end if ' Translate the raw index to one relative to the provided list subtitleSelIdx = getSubtitleSelIdxFromSubIdx(subtitles, subtitle_idx) selectedSubtitle = subtitles[subtitleSelIdx] if isValid(selectedSubtitle) and isValid(selectedSubtitle.IsEncoded) if selectedSubtitle.IsEncoded ' With encoded subtitles, turn off captions video.globalCaptionMode = "Off" else ' If this is a text-based subtitle, set relevant settings for roku captions video.globalCaptionMode = "On" video.subtitleTrack = video.availableSubtitleTracks[availSubtitleTrackIdx(video, subtitleSelIdx)].TrackName end if end if return subtitleSelIdx end function ' The subtitle index on the server differs from the index we track locally ' This function converts the former into the latter function getSubtitleSelIdxFromSubIdx(subtitles, sub_idx) as integer selIdx = 0 if sub_idx = -1 then return -1 for each item in subtitles if item.Index = sub_idx return selIdx end if selIdx = selIdx + 1 end for return -1 end function function selectSubtitleTrack(tracks, current = -1) as integer video = m.scene.focusedChild.focusedChild trackSelected = selectSubtitleTrackDialog(video.Subtitles, video.SelectedSubtitle) if trackSelected = invalid or trackSelected = -1 ' back pressed in Dialog - no selection made return -2 else return trackSelected - 1 end if end function ' Present Dialog to user to select subtitle track function selectSubtitleTrackDialog(tracks, currentTrack = -1) iso6392 = getSubtitleLanguages() options = ["None"] for each item in tracks forced = "" default = "" if item.IsForced then forced = " [Forced]" if item.IsDefault then default = " - Default" if isValid(item.Track.Language) language = iso6392.lookup(item.Track.Language) if language = invalid then language = item.Track.Language else language = "Undefined" end if options.push(language + forced + default) end for return option_dialog(options, "Select a subtitle track", currentTrack + 1) end function sub changeSubtitleDuringPlayback(newid) ' If no subtitles set if newid = invalid or newid = -1 turnoffSubtitles() return end if video = m.scene.focusedChild.focusedChild ' If no change of subtitle track, return if newid = video.SelectedSubtitle then return currentSubtitles = video.Subtitles[video.SelectedSubtitle] newSubtitles = video.Subtitles[newid] if newSubtitles.IsEncoded or (isValid(currentSubtitles) and currentSubtitles.IsEncoded) ' With encoded subtitles we need to stop/start playback video.control = "stop" AddVideoContent(video, video.mediaSourceId, video.audioIndex, newSubtitles.Index, video.position * 10000000) video.control = "play" else ' Switching from text to text (or none to text) does not require stopping playback video.globalCaptionMode = "On" video.subtitleTrack = video.availableSubtitleTracks[availSubtitleTrackIdx(video, newid)].TrackName end if video.SelectedSubtitle = newid end sub sub turnoffSubtitles() video = m.scene.focusedChild.focusedChild current = video.SelectedSubtitle video.SelectedSubtitle = -1 video.globalCaptionMode = "Off" device = CreateObject("roDeviceInfo") device.EnableAppFocusEvent(false) ' Check if Enoded subtitles are being displayed, and turn off if current > -1 and video.Subtitles[current].IsEncoded video.control = "stop" AddVideoContent(video, video.mediaSourceId, video.audioIndex, -1, video.position * 10000000) video.control = "play" end if end sub 'Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top function sortSubtitles(id as string, MediaStreams) tracks = { "forced": [], "default": [], "normal": [] } 'Too many args for using substitute prefered_lang = m.global.session.user.configuration.SubtitleLanguagePreference for each stream in MediaStreams if stream.type = "Subtitle" url = "" if isValid(stream.DeliveryUrl) url = buildURL(stream.DeliveryUrl) end if stream = { "Track": { "Language": stream.language, "Description": stream.displaytitle, "TrackName": url }, "IsTextSubtitleStream": stream.IsTextSubtitleStream, "Index": stream.index, "IsDefault": stream.IsDefault, "IsForced": stream.IsForced, "IsExternal": stream.IsExternal, "IsEncoded": stream.DeliveryMethod = "Encode" } if stream.isForced trackType = "forced" else if stream.IsDefault trackType = "default" else trackType = "normal" end if if prefered_lang <> "" and prefered_lang = stream.Track.Language tracks[trackType].unshift(stream) else tracks[trackType].push(stream) end if end if end for tracks["default"].append(tracks["normal"]) tracks["forced"].append(tracks["default"]) textTracks = [] for i = 0 to tracks["forced"].count() - 1 if tracks["forced"][i].IsTextSubtitleStream textTracks.push(tracks["forced"][i].Track) end if end for return { "all": tracks["forced"], "text": textTracks } end function function getSubtitleLanguages() return { "aar": "Afar", "abk": "Abkhazian", "ace": "Achinese", "ach": "Acoli", "ada": "Adangme", "ady": "Adyghe; Adygei", "afa": "Afro-Asiatic languages", "afh": "Afrihili", "afr": "Afrikaans", "ain": "Ainu", "aka": "Akan", "akk": "Akkadian", "alb": "Albanian", "ale": "Aleut", "alg": "Algonquian languages", "alt": "Southern Altai", "amh": "Amharic", "ang": "English, Old (ca.450-1100)", "anp": "Angika", "apa": "Apache languages", "ara": "Arabic", "arc": "Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)", "arg": "Aragonese", "arm": "Armenian", "arn": "Mapudungun; Mapuche", "arp": "Arapaho", "art": "Artificial languages", "arw": "Arawak", "asm": "Assamese", "ast": "Asturian; Bable; Leonese; Asturleonese", "ath": "Athapascan languages", "aus": "Australian languages", "ava": "Avaric", "ave": "Avestan", "awa": "Awadhi", "aym": "Aymara", "aze": "Azerbaijani", "bad": "Banda languages", "bai": "Bamileke languages", "bak": "Bashkir", "bal": "Baluchi", "bam": "Bambara", "ban": "Balinese", "baq": "Basque", "bas": "Basa", "bat": "Baltic languages", "bej": "Beja; Bedawiyet", "bel": "Belarusian", "bem": "Bemba", "ben": "Bengali", "ber": "Berber languages", "bho": "Bhojpuri", "bih": "Bihari languages", "bik": "Bikol", "bin": "Bini; Edo", "bis": "Bislama", "bla": "Siksika", "bnt": "Bantu (Other)", "bos": "Bosnian", "bra": "Braj", "bre": "Breton", "btk": "Batak languages", "bua": "Buriat", "bug": "Buginese", "bul": "Bulgarian", "bur": "Burmese", "byn": "Blin; Bilin", "cad": "Caddo", "cai": "Central American Indian languages", "car": "Galibi Carib", "cat": "Catalan; Valencian", "cau": "Caucasian languages", "ceb": "Cebuano", "cel": "Celtic languages", "cha": "Chamorro", "chb": "Chibcha", "che": "Chechen", "chg": "Chagatai", "chi": "Chinese", "chk": "Chuukese", "chm": "Mari", "chn": "Chinook jargon", "cho": "Choctaw", "chp": "Chipewyan; Dene Suline", "chr": "Cherokee", "chu": "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic", "chv": "Chuvash", "chy": "Cheyenne", "cmc": "Chamic languages", "cop": "Coptic", "cor": "Cornish", "cos": "Corsican", "cpe": "Creoles and pidgins, English based", "cpf": "Creoles and pidgins, French-based ", "cpp": "Creoles and pidgins, Portuguese-based ", "cre": "Cree", "crh": "Crimean Tatar; Crimean Turkish", "crp": "Creoles and pidgins ", "csb": "Kashubian", "cus": "Cushitic languages", "cze": "Czech", "dak": "Dakota", "dan": "Danish", "dar": "Dargwa", "day": "Land Dayak languages", "del": "Delaware", "den": "Slave (Athapascan)", "dgr": "Dogrib", "din": "Dinka", "div": "Divehi; Dhivehi; Maldivian", "doi": "Dogri", "dra": "Dravidian languages", "dsb": "Lower Sorbian", "dua": "Duala", "dum": "Dutch, Middle (ca.1050-1350)", "dut": "Dutch; Flemish", "dyu": "Dyula", "dzo": "Dzongkha", "efi": "Efik", "egy": "Egyptian (Ancient)", "eka": "Ekajuk", "elx": "Elamite", "eng": "English", "enm": "English, Middle (1100-1500)", "epo": "Esperanto", "est": "Estonian", "ewe": "Ewe", "ewo": "Ewondo", "fan": "Fang", "fao": "Faroese", "fat": "Fanti", "fij": "Fijian", "fil": "Filipino; Pilipino", "fin": "Finnish", "fiu": "Finno-Ugrian languages", "fon": "Fon", "fre": "French", "frm": "French, Middle (ca.1400-1600)", "fro": "French, Old (842-ca.1400)", "frc": "French (Canada)", "frr": "Northern Frisian", "frs": "Eastern Frisian", "fry": "Western Frisian", "ful": "Fulah", "fur": "Friulian", "gaa": "Ga", "gay": "Gayo", "gba": "Gbaya", "gem": "Germanic languages", "geo": "Georgian", "ger": "German", "gez": "Geez", "gil": "Gilbertese", "gla": "Gaelic; Scottish Gaelic", "gle": "Irish", "glg": "Galician", "glv": "Manx", "gmh": "German, Middle High (ca.1050-1500)", "goh": "German, Old High (ca.750-1050)", "gon": "Gondi", "gor": "Gorontalo", "got": "Gothic", "grb": "Grebo", "grc": "Greek, Ancient (to 1453)", "gre": "Greek, Modern (1453-)", "grn": "Guarani", "gsw": "Swiss German; Alemannic; Alsatian", "guj": "Gujarati", "gwi": "Gwich'in", "hai": "Haida", "hat": "Haitian; Haitian Creole", "hau": "Hausa", "haw": "Hawaiian", "heb": "Hebrew", "her": "Herero", "hil": "Hiligaynon", "him": "Himachali languages; Western Pahari languages", "hin": "Hindi", "hit": "Hittite", "hmn": "Hmong; Mong", "hmo": "Hiri Motu", "hrv": "Croatian", "hsb": "Upper Sorbian", "hun": "Hungarian", "hup": "Hupa", "iba": "Iban", "ibo": "Igbo", "ice": "Icelandic", "ido": "Ido", "iii": "Sichuan Yi; Nuosu", "ijo": "Ijo languages", "iku": "Inuktitut", "ile": "Interlingue; Occidental", "ilo": "Iloko", "ina": "Interlingua (International Auxiliary Language Association)", "inc": "Indic languages", "ind": "Indonesian", "ine": "Indo-European languages", "inh": "Ingush", "ipk": "Inupiaq", "ira": "Iranian languages", "iro": "Iroquoian languages", "ita": "Italian", "jav": "Javanese", "jbo": "Lojban", "jpn": "Japanese", "jpr": "Judeo-Persian", "jrb": "Judeo-Arabic", "kaa": "Kara-Kalpak", "kab": "Kabyle", "kac": "Kachin; Jingpho", "kal": "Kalaallisut; Greenlandic", "kam": "Kamba", "kan": "Kannada", "kar": "Karen languages", "kas": "Kashmiri", "kau": "Kanuri", "kaw": "Kawi", "kaz": "Kazakh", "kbd": "Kabardian", "kha": "Khasi", "khi": "Khoisan languages", "khm": "Central Khmer", "kho": "Khotanese; Sakan", "kik": "Kikuyu; Gikuyu", "kin": "Kinyarwanda", "kir": "Kirghiz; Kyrgyz", "kmb": "Kimbundu", "kok": "Konkani", "kom": "Komi", "kon": "Kongo", "kor": "Korean", "kos": "Kosraean", "kpe": "Kpelle", "krc": "Karachay-Balkar", "krl": "Karelian", "kro": "Kru languages", "kru": "Kurukh", "kua": "Kuanyama; Kwanyama", "kum": "Kumyk", "kur": "Kurdish", "kut": "Kutenai", "lad": "Ladino", "lah": "Lahnda", "lam": "Lamba", "lao": "Lao", "lat": "Latin", "lav": "Latvian", "lez": "Lezghian", "lim": "Limburgan; Limburger; Limburgish", "lin": "Lingala", "lit": "Lithuanian", "lol": "Mongo", "loz": "Lozi", "ltz": "Luxembourgish; Letzeburgesch", "lua": "Luba-Lulua", "lub": "Luba-Katanga", "lug": "Ganda", "lui": "Luiseno", "lun": "Lunda", "luo": "Luo (Kenya and Tanzania)", "lus": "Lushai", "mac": "Macedonian", "mad": "Madurese", "mag": "Magahi", "mah": "Marshallese", "mai": "Maithili", "mak": "Makasar", "mal": "Malayalam", "man": "Mandingo", "mao": "Maori", "map": "Austronesian languages", "mar": "Marathi", "mas": "Masai", "may": "Malay", "mdf": "Moksha", "mdr": "Mandar", "men": "Mende", "mga": "Irish, Middle (900-1200)", "mic": "Mi'kmaq; Micmac", "min": "Minangkabau", "mis": "Uncoded languages", "mkh": "Mon-Khmer languages", "mlg": "Malagasy", "mlt": "Maltese", "mnc": "Manchu", "mni": "Manipuri", "mno": "Manobo languages", "moh": "Mohawk", "mon": "Mongolian", "mos": "Mossi", "mul": "Multiple languages", "mun": "Munda languages", "mus": "Creek", "mwl": "Mirandese", "mwr": "Marwari", "myn": "Mayan languages", "myv": "Erzya", "nah": "Nahuatl languages", "nai": "North American Indian languages", "nap": "Neapolitan", "nau": "Nauru", "nav": "Navajo; Navaho", "nbl": "Ndebele, South; South Ndebele", "nde": "Ndebele, North; North Ndebele", "ndo": "Ndonga", "nds": "Low German; Low Saxon; German, Low; Saxon, Low", "nep": "Nepali", "new": "Nepal Bhasa; Newari", "nia": "Nias", "nic": "Niger-Kordofanian languages", "niu": "Niuean", "nno": "Norwegian Nynorsk; Nynorsk, Norwegian", "nob": "Bokmål, Norwegian; Norwegian Bokmål", "nog": "Nogai", "non": "Norse, Old", "nor": "Norwegian", "nqo": "N'Ko", "nso": "Pedi; Sepedi; Northern Sotho", "nub": "Nubian languages", "nwc": "Classical Newari; Old Newari; Classical Nepal Bhasa", "nya": "Chichewa; Chewa; Nyanja", "nym": "Nyamwezi", "nyn": "Nyankole", "nyo": "Nyoro", "nzi": "Nzima", "oci": "Occitan (post 1500); Provençal", "oji": "Ojibwa", "ori": "Oriya", "orm": "Oromo", "osa": "Osage", "oss": "Ossetian; Ossetic", "ota": "Turkish, Ottoman (1500-1928)", "oto": "Otomian languages", "paa": "Papuan languages", "pag": "Pangasinan", "pal": "Pahlavi", "pam": "Pampanga; Kapampangan", "pan": "Panjabi; Punjabi", "pap": "Papiamento", "pau": "Palauan", "peo": "Persian, Old (ca.600-400 B.C.)", "per": "Persian", "phi": "Philippine languages", "phn": "Phoenician", "pli": "Pali", "pol": "Polish", "pon": "Pohnpeian", "por": "Portuguese", "pob": "Portuguese (Brazil)", "pra": "Prakrit languages", "pro": "Provençal, Old (to 1500)", "pus": "Pushto; Pashto", "qaa-qtz": "Reserved for local use", "que": "Quechua", "raj": "Rajasthani", "rap": "Rapanui", "rar": "Rarotongan; Cook Islands Maori", "roa": "Romance languages", "roh": "Romansh", "rom": "Romany", "rum": "Romanian; Moldavian; Moldovan", "run": "Rundi", "rup": "Aromanian; Arumanian; Macedo-Romanian", "rus": "Russian", "sad": "Sandawe", "sag": "Sango", "sah": "Yakut", "sai": "South American Indian (Other)", "sal": "Salishan languages", "sam": "Samaritan Aramaic", "san": "Sanskrit", "sas": "Sasak", "sat": "Santali", "scn": "Sicilian", "sco": "Scots", "sel": "Selkup", "sem": "Semitic languages", "sga": "Irish, Old (to 900)", "sgn": "Sign Languages", "shn": "Shan", "sid": "Sidamo", "sin": "Sinhala; Sinhalese", "sio": "Siouan languages", "sit": "Sino-Tibetan languages", "sla": "Slavic languages", "slo": "Slovak", "slv": "Slovenian", "sma": "Southern Sami", "sme": "Northern Sami", "smi": "Sami languages", "smj": "Lule Sami", "smn": "Inari Sami", "smo": "Samoan", "sms": "Skolt Sami", "sna": "Shona", "snd": "Sindhi", "snk": "Soninke", "sog": "Sogdian", "som": "Somali", "son": "Songhai languages", "sot": "Sotho, Southern", "spa": "Spanish; Latin", "spa": "Spanish; Castilian", "srd": "Sardinian", "srn": "Sranan Tongo", "srp": "Serbian", "srr": "Serer", "ssa": "Nilo-Saharan languages", "ssw": "Swati", "suk": "Sukuma", "sun": "Sundanese", "sus": "Susu", "sux": "Sumerian", "swa": "Swahili", "swe": "Swedish", "syc": "Classical Syriac", "syr": "Syriac", "tah": "Tahitian", "tai": "Tai languages", "tam": "Tamil", "tat": "Tatar", "tel": "Telugu", "tem": "Timne", "ter": "Tereno", "tet": "Tetum", "tgk": "Tajik", "tgl": "Tagalog", "tha": "Thai", "tib": "Tibetan", "tig": "Tigre", "tir": "Tigrinya", "tiv": "Tiv", "tkl": "Tokelau", "tlh": "Klingon; tlhIngan-Hol", "tli": "Tlingit", "tmh": "Tamashek", "tog": "Tonga (Nyasa)", "ton": "Tonga (Tonga Islands)", "tpi": "Tok Pisin", "tsi": "Tsimshian", "tsn": "Tswana", "tso": "Tsonga", "tuk": "Turkmen", "tum": "Tumbuka", "tup": "Tupi languages", "tur": "Turkish", "tut": "Altaic languages", "tvl": "Tuvalu", "twi": "Twi", "tyv": "Tuvinian", "udm": "Udmurt", "uga": "Ugaritic", "uig": "Uighur; Uyghur", "ukr": "Ukrainian", "umb": "Umbundu", "und": "Undetermined", "urd": "Urdu", "uzb": "Uzbek", "vai": "Vai", "ven": "Venda", "vie": "Vietnamese", "vol": "Volapük", "vot": "Votic", "wak": "Wakashan languages", "wal": "Walamo", "war": "Waray", "was": "Washo", "wel": "Welsh", "wen": "Sorbian languages", "wln": "Walloon", "wol": "Wolof", "xal": "Kalmyk; Oirat", "xho": "Xhosa", "yao": "Yao", "yap": "Yapese", "yid": "Yiddish", "yor": "Yoruba", "ypk": "Yupik languages", "zap": "Zapotec", "zbl": "Blissymbols; Blissymbolics; Bliss", "zen": "Zenaga", "zgh": "Standard Moroccan Tamazight", "zha": "Zhuang; Chuang", "znd": "Zande languages", "zul": "Zulu", "zun": "Zuni", "zxx": "No linguistic content; Not applicable", "zza": "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki" } end function