├── README.md ├── READMEde.md ├── changelog.txt └── src ├── main.lua ├── matroskaplayback.lua └── mpvmatroska.lua /README.md: -------------------------------------------------------------------------------- 1 | # mpvMatroska 2 | 3 | mpvMatroska is a Lua script for mpv which almost turns mpv into a full Matroska player. 4 | 5 | The project is under construction and not much is working yet and a lot will change over time. 6 | Nevertheless, there are already some working Matroska features. 7 | 8 | ## mpvMatroska development 9 | 10 | To test the Matroska features I use my [Matroska-Playback](https://github.com/hubblec4/Matroska-Playback) repo. 11 | 12 | ## Installation 13 | 14 | mpvMatroska uses some other Lua modules. 15 | 16 | - [LuaEBML](https://github.com/hubblec4/LuaEBML) 17 | - [LuaMatroska](https://github.com/hubblec4/LuaMatroska) 18 | - [LuaMatroskaParser](https://github.com/hubblec4/LuaMatroskaParser) 19 | 20 | But to keep everything simple, mpvMatroska uses the `mpv folder system` for scripts that consist of several modules. 21 | 22 | All you have to do is copy the mpvMatroska folder into the mpv-scripts folder. 23 | 24 | The current version of mpvMatroska can be downloaded in the `Releases`. 25 | 26 | ### uosc 27 | 28 | mpvMatroska uses the GUI from [uosc](https://github.com/tomasklaen/uosc) to display lists and buttons. 29 | mpvMatroska still works without uosc, but then you don't have any selection lists. 30 | 31 | ## working Matroska features 32 | 33 | - Hard-Linking 34 | - Nested chapters 35 | - Ordered chapters 36 | - Nested-Ordered chapters 37 | - Linking chapters (with chapter duration) 38 | - Linked-Edition chapters 39 | - Multiple chapter names 40 | - Multiple edition names 41 | - Video rotation (with `ProjectionPoseRoll` element) 42 | - MatroskaScript menu 43 | 44 | ## mpvMatroska features 45 | 46 | - Automatic switching of edition and chapter names when the audio or subtitle track is changed 47 | - Improved title display: the current edition name is appended to the file name or the file Title(if present) 48 | - Correct edition list: the standard short-cut "E" is processed internally using its own switching method 49 | - Video rotation: with the Matroska Tags (not official in the Matroska specifications) 50 | - Matroska Content-Grouping: not yet officially included in the Matroska specifications 51 | 52 | ### Matroska Content-Grouping 53 | 54 | This Matroska feature is about grouping the content in a Matroska file in a meaningful way. 55 | The content is all track types as well as the editions/chapters. 56 | This is the basic basis of Haali TRACKSETEX. 57 | The Matroska Attachments could be used as additional content, for example to specifically load a font for a subtitle. 58 | 59 | The hotkey `g` is available to switch through existing content groups. 60 | Furthermore, a freely selectable hotkey can be set up in the mpv `input.conf` using the `script-message-to` system. 61 | 62 | To switch the content groups with the "k" key, the following line is used. 63 | 64 | ```text 65 | k script-message-to mpvMatroska cycle-contentgroups 66 | ``` 67 | 68 | If uosc is installed you can open the selection menu with the hotkey `ALT+g`. 69 | You can also assign your own hotkey to open the selection menu. 70 | 71 | ```text 72 | G script-message-to mpvMatroska open-contentgroups 73 | ``` 74 | 75 | You can also install your own button in the control menu in uosc. 76 | To do this you have to adapt the line `controls=` in `uosc.conf`. 77 | I use the following code for this. 78 | 79 | ```text 80 | command:hub:script-binding mpvMatroska/open-contentgroups?Content-Groups 81 | ``` 82 | 83 | ### Matroska Editions 84 | 85 | Currently mpv doesn't handle multiple editions correctly and even creates an incorrect list when linking chapters come into play. 86 | Therefore, the mpv internal method should not be used to switch editions. 87 | 88 | You can also assign a separate hotkey to switch between editions. 89 | 90 | ```text 91 | your-key script-message-to mpvMatroska cycle-editions 92 | ``` 93 | 94 | A separate editions button can (and should) be created in the uosc control menu. 95 | To do this you have to adapt the line `controls=` in `uosc.conf`. 96 | The predefined standard entry must be replaced. 97 | 98 | ```text 99 | // original entry: 100 | editions 101 | 102 | // to be replaced with: 103 | command:bookmarks:script-binding mpvMatroska/open-editions?Editions 104 | ``` 105 | -------------------------------------------------------------------------------- /READMEde.md: -------------------------------------------------------------------------------- 1 | # mpvMatroska 2 | 3 | mpvMatroska ist ein Lua script für mpv wodurch mpv nahezu in einen vollständigen Matroska Player verwandelt wird. 4 | 5 | Das Projekt befindet sich im Aufbau und es funktioniert auch noch nicht viel und vieles wird sich ändern mit der Zeit. 6 | Dennoch gibt es auch schon einige funktionierende Matroska Features. 7 | 8 | ## mpvMatroska Entwicklung 9 | 10 | Zum Testen der Matroska Features nutze ich mein [Matroska-Playback](https://github.com/hubblec4/Matroska-Playback) Repo. 11 | 12 | ## Installation 13 | 14 | mpvMatroska benutzt einige andere Lua Module. 15 | 16 | - [LuaEBML](https://github.com/hubblec4/LuaEBML) 17 | - [LuaMatroska](https://github.com/hubblec4/LuaMatroska) 18 | - [LuaMatroskaParser](https://github.com/hubblec4/LuaMatroskaParser) 19 | 20 | Damit aber alles einfach bleibt nutzt mpvMatroska das `mpv Ordner System` für Scripte die aus mehreren Modulen bestehen. 21 | 22 | Alles was man tun muss, ist den mpvMatroska Ordner in den mpv Scripte Ordner kopieren. 23 | 24 | Die aktuelle Version von mpvMatroska kann in den `Releases` heruntergeladen werden. 25 | 26 | ### uosc 27 | 28 | mpvMatroska nutzt die GUI von [uosc](https://github.com/tomasklaen/uosc) für die Darstellung von Listen und Buttons. 29 | mpvMatroska funktioniert auch weiterhin ohne uosc, allerdings hat man dann keine Auswahllisten. 30 | 31 | ## funktionierende Matroska Features 32 | 33 | - Hard-Linking 34 | - Verschachtelte Kapitel 35 | - Reihenfolgentreue Kapitel 36 | - Verschachtelte-Reihenfolgentreue Kapitel 37 | - Verknüpfende Kapitel (mit Kapitel Dauer) 38 | - Verknüpfte-Version Kapitel 39 | - Multiple Kapitelnamen 40 | - Multiple Versionsnamen 41 | - Video Rotation (mit `ProjectionPoseRoll` Element) 42 | - MatroskaScript Menü 43 | 44 | ## mpvMatroska Features 45 | 46 | - Automatisches Umschalten der Versions- und Kapitel Namen wenn die Audio- oder Untertitelspur gewechselt wird 47 | - Verbesserte Titel Anzeige: der aktuelle Versionsname wird an den Dateinamen oder den Datei Titel(wenn vorhanden) angehängt 48 | - Korrekte Versionen Liste: der Standard Short-Cut "E" wird intern mit einer eigenen Durchschalt-Methode verarbeitet 49 | - Video Rotation: mit den Matroska Tags (nicht offiziell in den Matroska Spezifikationen) 50 | - Matroska Inhaltsgruppierung: noch nicht offiziell in den Matroska Spezifikationen enthalten 51 | 52 | ### Matroska Inhaltsgruppierung 53 | 54 | Bei dieser Matroska Eigenschaft geht es darum den Inhalt in einer Matroska Datei sinnvoll zu gruppieren. 55 | Der Inhalt sind alle Spurtypen sowie die Versionen/Kapitel. 56 | Dies ist die Grundbasis von Haali TRACKSETEX. 57 | Als weiterer Inhalt könnten noch die Matroska Anhänge verwendet werden, um zum Beispiel gezielt eine Schriftart für einen Untertitel zu laden. 58 | 59 | Für das Durchschalten vorhandener Inhaltsgruppen ist der Hotkey `g` verfügbar. 60 | Weiterhin kann in der mpv `input.conf` ein frei wählbarer Hotkey eingerichtet werden mittels dem `script-message-to` System. 61 | 62 | Um die Inhaltsgruppen mit der Taste "k" umzuschalten wird folgende Zeile verwendet. 63 | 64 | ```text 65 | k script-message-to mpvMatroska cycle-contentgroups 66 | ``` 67 | 68 | Wenn uosc installiert ist kann man mit dem Hotkey `ALT+g` das Auswahlmenü öffnen. 69 | Ebenso kann ein eigener Hotkey vergeben werden um das Auswahlmenü zu öffnen. 70 | 71 | ```text 72 | G script-message-to mpvMatroska open-contentgroups 73 | ``` 74 | 75 | Weiterhin kann man in uosc einen eigenen Button im Kontrollmenü einbauen. 76 | Dazu muss man in der `uosc.conf` die Zeile `controls=` anpassen. 77 | Ich nutze dazu folgenden code. 78 | 79 | ```text 80 | command:hub:script-binding mpvMatroska/open-contentgroups?Content-Groups 81 | ``` 82 | 83 | ### Matroska Versionen 84 | 85 | Momentan verarbeitet mpv multiple Versionen nicht korrekt und erstellt sogar eine falsche Liste wenn verknüpfende Kapitel ins Spiel kommen. 86 | Daher sollte zum Umschaten der Versionen nicht die mpv interne Methode genutzt werden. 87 | 88 | Für das Durchschalten der Versionen kann ebenfalls ein eigener Hotkey vergeben werden. 89 | 90 | ```text 91 | your-key script-message-to mpvMatroska cycle-editions 92 | ``` 93 | 94 | Im uosc Kontrollmenü kann (und sollte) ein eigener Versionen Button erstellt werden. 95 | Dazu muss man in der `uosc.conf` die Zeile `controls=` anpassen. 96 | Der vordefiniert Standard Eintrag muss ersetzt werden. 97 | 98 | ```text 99 | // originaler Eintrag: 100 | editions 101 | 102 | // zu ersetzen mit: 103 | command:bookmarks:script-binding mpvMatroska/open-editions?Editions 104 | ``` 105 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | mpvMatroska change log 2 | 3 | & added 4 | * fixed 5 | # improved 6 | % changed 7 | 8 | 9 | rev 1 2023-11-22 10 | & support for MatroskaScript menu -> GotoAndPlay( ChapterUID ); 11 | 12 | % the Matroska file Title is now always used from the main input file 13 | 14 | 15 | init - 2023-11-10 16 | 17 | * Hidden editions are now processed correctly 18 | 19 | % support for uosc 5.x.x, older versions no longer supported 20 | -------------------------------------------------------------------------------- /src/main.lua: -------------------------------------------------------------------------------- 1 | require "mpvmatroska" 2 | -------------------------------------------------------------------------------- /src/matroskaplayback.lua: -------------------------------------------------------------------------------- 1 | -- Matroska Playback 2 | -- a library to handle Matoska features for playback with mpv 3 | -- written by hubblec4 4 | 5 | local mk = require "matroska" 6 | local mkp = require "matroskaparser" 7 | local mp = require "mp" 8 | local msg = require "mp.msg" 9 | local utils = require "mp.utils" 10 | 11 | 12 | -- constants ------------------------------------------------------------------- 13 | MK_FEATURE = { 14 | hard_linking = 1, -- a special system to link files virtually to a single seamless timeline 15 | basic_chapters = 2, -- chapters with only a start time 16 | nested_chapters = 3, -- chapters within chapters 17 | ordered_chapters = 4, -- chapters uses start and end time and are played in the given order of the chapters 18 | nested_ordered_chapters = 5, -- a combination of nested_ and ordered_ chapters - not fully implemented in the specs 19 | linked_chapters = 6, -- same as "ordered_chapters" and additional is the content linked to other files 20 | linked_editions = 7, -- same as "linked chapters" but an entire edition is linked to play, a duration of this chapter is ignored 21 | linked_chapters_file = 8, -- a file without Tracks, only linked chapters are used 22 | multiple_editions = 17, -- more than one edition is present 23 | multiple_edition_names = 55, -- there is more than one edition name 24 | multiple_chapter_names = 56, -- there is more than one chapter name 25 | native_menu = 90, -- Matroska Native Menu -> ChapterProcessCodecID = 0 26 | dvd_menu = 91, -- Matroska DVD Menu -> ChapterProcessCodecID = 1 27 | haali_TRACKSETEX = 99, -- Haali's TRACKSETEX, not official Matroska 28 | -- other features later 29 | } 30 | 31 | 32 | -- gets the directory section of the given path 33 | local function get_directory(path) 34 | return path:match("^(.+[/\\])[^/\\]+[/\\]?$") or "" 35 | end 36 | 37 | -- gets the file name of the given path 38 | local function get_filename(path) 39 | return path:match("^.+[/\\](.+)$") or path 40 | end 41 | 42 | 43 | 44 | -- ----------------------------------------------------------------------------- 45 | -- Internal Edition Class ------------------------------------------------------ 46 | -- ----------------------------------------------------------------------------- 47 | 48 | local Internal_Edition = { 49 | edl_path = "", 50 | current_lang = "", 51 | current_name = "", 52 | duration = 0, 53 | ordered = false, -- edition use ordered chapters? 54 | hidden = false, 55 | } 56 | 57 | -- constructor 58 | function Internal_Edition:new() 59 | local elem = {} 60 | setmetatable(elem, self) 61 | self.__index = self 62 | elem.chapter_timeline = {} -- array, is a linear chapter list of all used chapters, currently exluded disabled chapters 63 | return elem 64 | end 65 | 66 | -- add_chapter_segment 67 | function Internal_Edition:add_chapter_segment(chp_ref, file_path, time_marker, level, hidden) 68 | local c_segment = { 69 | file_path = file_path, -- file path is needed to create the EDL path 70 | timeline_marker = time_marker, -- float with seconds, this is also a new virtual start time when the referenced chapter is played 71 | level = level, -- nested level of the chapter 72 | current_lang = "_nil_", -- init with an invalid language code 73 | current_name = "", 74 | Chapter = chp_ref, -- a chapter reference 75 | -- visible: boolean, this flag overrides the internal ChapterFlagHidden 76 | -- this can happen when linked-Editions are used and the linked Edition is hidden 77 | visible = not hidden, 78 | -- virtual: boolean, such a chapter is used to link a file without chapters 79 | -- or for Hard-Linking to skip ordered Editions in the chain 80 | -- or when a first chapter's start time of a non-ordered Edition is not 0 81 | virtual = false, 82 | 83 | } 84 | table.insert(self.chapter_timeline, c_segment) 85 | return #self.chapter_timeline 86 | end 87 | 88 | -- add_virtual_segment 89 | function Internal_Edition:add_virtual_segment(file_path, time_marker, level, chap_start, chap_end) 90 | if chap_start == nil then chap_start = 0 end -- check start time 91 | 92 | -- create a virtual chapter and set the times 93 | local vc = mk.chapters.ChapterAtom:new() 94 | -- set start time 95 | vc:get_child(mk.chapters.ChapterTimeStart).value = chap_start 96 | -- end time, this vaule is not always there (non-ordered editions) 97 | if chap_end then 98 | local et = mk.chapters.ChapterTimeEnd:new() 99 | et.value = chap_end 100 | table.insert(vc.value, et) 101 | end 102 | 103 | local idx = self:add_chapter_segment(vc, file_path, time_marker, level, true) 104 | self.chapter_timeline[idx].virtual = true 105 | return idx 106 | end 107 | 108 | -- add_chap_endtime: a method to set a chapter end time of non-ordered editions 109 | -- with ordered chapters it is everything easy to handle 110 | function Internal_Edition:add_chap_endtime(idx, time) 111 | -- check if a end time element is already there 112 | 113 | local et = self.chapter_timeline[idx].Chapter:find_child(mk.chapters.ChapterTimeEnd) 114 | -- end time is present, change the value to new time 115 | if et then 116 | et.value = time 117 | return 118 | end 119 | 120 | -- create new end time element 121 | et = mk.chapters.ChapterTimeEnd:new() 122 | et.value = time 123 | table.insert(self.chapter_timeline[idx].Chapter.value, et) 124 | end 125 | 126 | -- get_mpv_chapter_list() 127 | function Internal_Edition:get_mpv_chapter_list(langs) 128 | -- langs: array with language codes, when nil the first name is used 129 | if langs == nil then langs = {""} end -- no language code -> first name 130 | local mpvlist = {} -- mpv chapter list 131 | local c_name 132 | 133 | -- loop over chapter items 134 | for _, chp_item in ipairs(self.chapter_timeline) do 135 | -- chapters must be visible, check item.visible and chapter hidden flag 136 | if chp_item.visible and not chp_item.Chapter:is_hidden() then 137 | c_name = "" 138 | 139 | -- find chapter name 140 | for _, lng in ipairs(langs) do 141 | -- check lng, nothing todo if the current_lang match 142 | if lng == chp_item.current_lang then break end 143 | 144 | c_name = chp_item.Chapter:get_name(lng, false, true) 145 | 146 | -- name found 147 | if c_name ~= "" then 148 | chp_item.current_name = c_name 149 | chp_item.current_lang = lng -- set new language 150 | break 151 | end 152 | end 153 | 154 | -- no new name found and current_name is empty 155 | if c_name == "" and chp_item.current_name == "" then 156 | c_name = chp_item.Chapter:get_name("") -- get first chapter name 157 | -- check again, name could be empty -> no chapter Display or a really empty name 158 | if c_name == "" then c_name = " " end -- use a space for the name 159 | chp_item.current_name = c_name 160 | chp_item.current_lang = "" 161 | end 162 | 163 | -- process nested level 164 | if chp_item.level > 0 then 165 | c_name = " " 166 | for i = 1, chp_item.level do 167 | c_name = c_name .. "+" 168 | end 169 | c_name = c_name .. " " .. chp_item.current_name 170 | else 171 | c_name = chp_item.current_name 172 | end 173 | 174 | -- add chapter to the mpvlist 175 | table.insert(mpvlist, {time = chp_item.timeline_marker / 1000000000, title = c_name}) 176 | end 177 | end 178 | 179 | return mpvlist 180 | end 181 | 182 | -- get_main_filepath: returns the file path from the first chapter segment 183 | function Internal_Edition:get_main_filepath() 184 | return self.chapter_timeline[1].file_path 185 | end 186 | 187 | -- create_edl_path 188 | function Internal_Edition:create_edl_path() 189 | -- non-ordered edition 190 | if not self.ordered then 191 | local filepath = self.chapter_timeline[1].file_path -- file path from frist chapter segment 192 | self.edl_path = ("edl://!no_chapters;%%%d%%%s;"):format(filepath:len(), filepath) 193 | return 194 | end 195 | 196 | -- all chapters should now have ever an end time 197 | local start_time = 0 198 | local prev_endtime = 0 199 | local curr_endtime = 0 200 | local curr_path = "" 201 | self.edl_path = "edl://!no_chapters;" -- init edl path 202 | 203 | -- loop over chapter items 204 | for _, chp_item in ipairs(self.chapter_timeline) do 205 | -- start time 206 | local curr_starttime = chp_item.Chapter:get_child(mk.chapters.ChapterTimeStart).value 207 | -- end time 208 | curr_endtime = chp_item.Chapter:find_child(mk.chapters.ChapterTimeEnd).value 209 | 210 | -- new path, a new file segment or different times 211 | if chp_item.file_path ~= curr_path or prev_endtime ~= curr_starttime then 212 | 213 | if prev_endtime > start_time then 214 | self.edl_path = self.edl_path .. ("%%%d%%%s,%f,%f;"):format(curr_path:len(), 215 | curr_path, start_time / 1000000000, (prev_endtime - start_time) / 1000000000) 216 | end 217 | 218 | -- new times 219 | start_time = curr_starttime 220 | prev_endtime = curr_endtime 221 | -- new file path 222 | if chp_item.file_path ~= curr_path then 223 | curr_path = chp_item.file_path -- set new current path 224 | end 225 | 226 | else -- same file path and sequential times 227 | prev_endtime = curr_endtime 228 | end 229 | 230 | end 231 | 232 | -- last chapter 233 | if curr_endtime > start_time then 234 | self.edl_path = self.edl_path .. ("%%%d%%%s,%f,%f;"):format(curr_path:len(), 235 | curr_path, start_time / 1000000000, (curr_endtime - start_time) / 1000000000) 236 | end 237 | end 238 | 239 | 240 | -- find chapter by UID: returns the internal chapter segment 241 | function Internal_Edition:find_chapter_byUID(uid) 242 | -- this method will be needed for the GotoAndPlay() command 243 | for i, intern_chap in ipairs(self.chapter_timeline) do 244 | if not intern_chap.virtual then -- ignore virtaul chapter segments 245 | if intern_chap.Chapter:get_child(mk.chapters.ChapterUID).value == uid then 246 | return intern_chap 247 | end 248 | end 249 | end 250 | 251 | return nil 252 | end 253 | 254 | -- find chapter by time: returns the internal chapter segment and a rest duration of this chapter 255 | function Internal_Edition:find_chapter_byTime(ns_time) 256 | -- find a chapter segment by it's chapter timeline marker time 257 | -- time is in nano seconds and is a range from start to end time of the chapter 258 | if ns_time == nil or ns_time < 0 then return nil end 259 | 260 | for i, intern_chap in ipairs(self.chapter_timeline) do 261 | if intern_chap.timeline_marker > ns_time then 262 | return self.chapter_timeline[i - 1], intern_chap.timeline_marker - ns_time -- return prev chapter and rest duration 263 | end 264 | end 265 | -- last chapter 266 | return self.chapter_timeline[#self.chapter_timeline], self.duration - ns_time 267 | end 268 | 269 | 270 | 271 | -- ----------------------------------------------------------------------------- 272 | -- Matroska Playback class ----------------------------------------------------- 273 | -- ----------------------------------------------------------------------------- 274 | 275 | local Mk_Playback = { 276 | -- init_file: a parsed first file 277 | init_file = nil, 278 | 279 | -- init_dir: directory where the init_file is located 280 | init_dir = "", 281 | 282 | -- mk_files: a list of parsed files which are needed for playback 283 | mk_files = {}, 284 | 285 | -- temp_files: a list of parsed files 286 | temp_files = {}, 287 | 288 | -- used_features: a list of used Matroska features 289 | used_features = {}, 290 | 291 | -- edl_path: a string with a special path system for mpv 292 | edl_path = "", 293 | 294 | -- mpv_current_vid: mpv number for video streams, start with 1, 0 = not used 295 | mpv_current_vid = 0, 296 | 297 | -- mpv_current_aid: mpv number for audio streams, start with 1, 0 = not used 298 | mpv_current_aid = 0, 299 | 300 | -- mpv_current_sid: mpv number for subtitle streams, start with 1, 0 = not used 301 | mpv_current_sid = 0, 302 | 303 | -- available_chapters_langs: array[lng = true] 304 | available_chapters_langs = {}, 305 | 306 | -- current_edition_idx: integer, 1-based 307 | current_edition_idx = 0, 308 | 309 | -- internal_editions: array, a list of edition items with useful info 310 | internal_editions = {}, 311 | 312 | -- edition_is_changing: boolean 313 | edition_is_changing = false, 314 | 315 | -- content_group_idx: integer 316 | content_group_idx = 0, -- needed for cycle 317 | } 318 | 319 | -- constructor 320 | function Mk_Playback:new(path) 321 | local elem = {} 322 | setmetatable(elem, self) 323 | self.__index = self 324 | elem.init_file = nil 325 | elem.mk_files = {} 326 | elem.temp_files = {} 327 | elem.used_features = {} 328 | elem.available_chapters_langs = {} 329 | elem.internal_editions = {} 330 | -- content_groups: array of content_group{name: string; vid?: integer; aid?: integer; sid?: integer; eid?: integer} 331 | elem.content_groups = {} 332 | elem:_scan(path) 333 | return elem 334 | end 335 | 336 | -- close: clean some var's 337 | function Mk_Playback:close() 338 | if self.init_file then self.init_file:close() end 339 | self:_close_files() 340 | end 341 | 342 | -- current_edition: returns the current used internal edition 343 | function Mk_Playback:current_edition() 344 | if self.current_edition_idx == 0 then return nil end 345 | return self.internal_editions[self.current_edition_idx] 346 | end 347 | 348 | -- get_mpv_chapters: returns the mpv chapters list and sets the edition names 349 | -- @param init: boolean, is used for initialization reason 350 | -- @param pref_subs: boolean, is used for subtitle change to use this language first 351 | function Mk_Playback:get_mpv_chapters(init, pref_subs) 352 | local file, trk, lng 353 | local langs = {} 354 | 355 | file = self:_get_main_file(true) -- get main file for tracks 356 | if file then 357 | -- audio language 358 | if self.mpv_current_aid > 0 then 359 | trk = file:get_audio(self.mpv_current_aid) 360 | if trk then 361 | lng = trk:get_language() 362 | if self.available_chapters_langs[lng] then 363 | table.insert(langs, lng) 364 | end 365 | end 366 | end 367 | 368 | -- subtitle language 369 | if self.mpv_current_sid > 0 then 370 | trk = file:get_subtitle(self.mpv_current_sid) 371 | if trk then 372 | lng = trk:get_language() 373 | if self.available_chapters_langs[lng] then 374 | -- check pref_subs 375 | if pref_subs then 376 | table.insert(langs, 1, lng) -- add in first positon 377 | else 378 | table.insert(langs, lng) 379 | end 380 | end 381 | end 382 | end 383 | end 384 | 385 | -- empty langs list 386 | if #langs == 0 then 387 | -- without a language it makes no sense to create the chapters list 388 | -- it will be the same as current used 389 | 390 | -- init is true 391 | if init then 392 | langs = nil -- uses nil, this will be change to an empty language -> first chapter name 393 | 394 | else -- init is false/nil 395 | return nil -- nil signals there is no change necessary of the current chapter list 396 | end 397 | end 398 | 399 | -- set edition names 400 | if init or self.used_features[MK_FEATURE.multiple_edition_names] then 401 | self:_set_edition_names(langs) 402 | end 403 | 404 | -- get the mpv chapter list 405 | if init or self.used_features[MK_FEATURE.multiple_chapter_names] then 406 | return self:current_edition():get_mpv_chapter_list(langs) 407 | 408 | else return nil 409 | end 410 | end 411 | 412 | 413 | -- edition_changing: returns boolean, a method to change the edition 414 | function Mk_Playback:edition_changing(new_idx) 415 | if new_idx == nil or new_idx == self.current_edition_idx or new_idx > #self.internal_editions 416 | or not self.used_features[MK_FEATURE.multiple_editions] then return false end 417 | 418 | -- new_idx: integer, 0 means toggle, all other values means the index 419 | if new_idx == 0 then 420 | -- toggle editions: this is a user option and hidden editions must be skipped 421 | -- if all editions are hidden -> no toggle 422 | new_idx = self.current_edition_idx 423 | while true do 424 | -- get next index 425 | if new_idx == #self.internal_editions then 426 | new_idx = 1 else new_idx = new_idx +1 427 | end 428 | -- check index - is current edition index? 429 | if new_idx == self.current_edition_idx then return false end -- no next not hidden edition found 430 | 431 | -- check hidden state 432 | if not self.internal_editions[new_idx].hidden then 433 | self.current_edition_idx = new_idx 434 | break 435 | end 436 | end 437 | 438 | else 439 | self.current_edition_idx = new_idx -- set new index 440 | end 441 | 442 | -- set active edl_path 443 | self.edl_path = self.internal_editions[self.current_edition_idx].edl_path 444 | 445 | self.edition_is_changing = true -- changing edition is now true 446 | 447 | -- load the edition with the new edl path 448 | mp.commandv("loadfile", self.edl_path, "replace") 449 | 450 | return true 451 | 452 | --TODO: for later: check switching Angle-Editions 453 | -- -> Bluray or DVD Angle Movies all Editions have the same duration. after a change restore the position 454 | end 455 | 456 | 457 | -- video_rotation: a method to rotate automatically the current video 458 | function Mk_Playback:video_rotation() 459 | if self.mpv_current_vid == 0 then return end 460 | local mk_file = self:_get_main_file(true) 461 | if not mk_file then return end 462 | 463 | -- check rotation settings 464 | local rotate, native = mk_file:get_video_rotation(self.mpv_current_vid) 465 | 466 | -- check native: a native rotate vaule is a float from -180.0 to 180.0 467 | if native then 468 | -- first we need an integer, and second the negative values must be transformed 469 | rotate = math.floor(rotate) 470 | 471 | -- check negative numbers 472 | if rotate < 0 then 473 | rotate = rotate + 360 474 | end 475 | end 476 | 477 | -- send the mpv rotate command 478 | if mp.get_property_number("video-rotate") ~= rotate then 479 | mp.set_property_number("video-rotate", rotate) 480 | end 481 | end 482 | 483 | -- mpv events ------------------------------------------------------------------ 484 | 485 | -- mpv_on_video_change: event when in mpv the video track is changed 486 | function Mk_Playback:mpv_on_video_change(new_id) 487 | -- no video selected 488 | if new_id == nil then 489 | self.mpv_current_vid = 0 490 | return 491 | end 492 | -- same video id 493 | if new_id == self.mpv_current_vid then return end 494 | 495 | self.mpv_current_vid = new_id -- set new video id 496 | 497 | -- check video rotation 498 | self:video_rotation() 499 | end 500 | 501 | -- mpv_on_audio_change: event when in mpv the audio track is changed 502 | function Mk_Playback:mpv_on_audio_change(new_id) 503 | -- no audio selected 504 | if new_id == nil then 505 | self.mpv_current_aid = 0 506 | return 507 | end 508 | -- same audio id 509 | if new_id == self.mpv_current_aid then return end 510 | 511 | self.mpv_current_aid = new_id -- set new audio id 512 | 513 | -- check and set multiple names 514 | self:_change_edition_chapter_names() 515 | end 516 | 517 | -- mpv_on_subtitle_change: event when in mpv the subtitle track is changed 518 | function Mk_Playback:mpv_on_subtitle_change(new_id) 519 | -- no subtitle selected 520 | if new_id == nil then 521 | self.mpv_current_sid = 0 522 | return 523 | end 524 | -- same subtitle id 525 | if new_id == self.mpv_current_sid then return end 526 | 527 | self.mpv_current_sid = new_id -- set new subtitle id 528 | 529 | -- check and set multiple names 530 | self:_change_edition_chapter_names(true) 531 | end 532 | 533 | 534 | 535 | -- mpv_set_media_title: a method to change the media title in the OSC and mpv window 536 | function Mk_Playback:mpv_set_media_title() 537 | local title = self:_get_main_file():get_title() -- get title from main input file 538 | if title == "" then title = get_filename(self:_get_main_file().path) end -- no title found, use the filename 539 | 540 | if self.used_features[MK_FEATURE.multiple_editions] 541 | or self.used_features[MK_FEATURE.multiple_edition_names] then 542 | local ed_name = self:current_edition().current_name 543 | if ed_name ~= "" then 544 | title = title .. " {" .. ed_name .. "}" 545 | end 546 | end 547 | mp.set_property("force-media-title", title) 548 | end 549 | 550 | 551 | -- cycle_content_groups: cycles the content groups 552 | function Mk_Playback:cycle_content_groups() 553 | if not self.used_features[MK_FEATURE.haali_TRACKSETEX] then return end 554 | 555 | if self.content_group_idx == #self.content_groups or self.content_group_idx == 0 then 556 | self.content_group_idx = 1 557 | else 558 | self.content_group_idx = self.content_group_idx + 1 559 | end 560 | self:_set_content_from_group(self.content_group_idx) 561 | end 562 | 563 | 564 | -- observe_playback_time: a method to react to the current play time 565 | function Mk_Playback:observe_playback_time(playtime) 566 | -- if Matroska Native Menu is used there are chapters with commands -> find them 567 | 568 | -- playtime in nanoseconds 569 | playtime = math.floor(playtime * 1e9) 570 | 571 | local intern_chap, rest = self:current_edition():find_chapter_byTime(playtime) 572 | if intern_chap == nil or intern_chap.virtual then return end 573 | 574 | -- determine process time 575 | local process_time = mk.CHAP_PROCESS_TIME.DURING -- during as init value 576 | if playtime >= intern_chap.timeline_marker and playtime < intern_chap.timeline_marker + 2e7 then -- a range of 20 milli seconds 577 | process_time = mk.CHAP_PROCESS_TIME.BEFORE 578 | 579 | elseif rest <= 2e7 then -- 20 milli seconds rest playtime of this chapter 580 | process_time = mk.CHAP_PROCESS_TIME.AFTER 581 | end 582 | 583 | self:_chapter_process_handler(intern_chap, process_time) 584 | --mp.osd_message(intern_chap.current_name .. " - " .. tostring(playtime)) -- for testing 585 | end 586 | 587 | 588 | 589 | -- uosc GUI section ------------------------------------------------------------ 590 | --[[ Menu data structure 591 | Menu { 592 | type?: string; 593 | title?: string; 594 | items: Item[]; 595 | selected_index?: integer; 596 | keep_open?: boolean; 597 | on_close?: string | string[]; 598 | on_search?: string | string[]; 599 | palette?: boolean; 600 | search_debounce?: 'submit' | number; 601 | search_suggestion?: string; 602 | } 603 | 604 | Item = Command | Submenu; 605 | 606 | Submenu { 607 | title?: string; 608 | hint?: string; 609 | items: Item[]; 610 | keep_open?: boolean; 611 | on_search?: string | string[]; 612 | palette?: boolean; 613 | search_debounce?: 'submit' | number; 614 | search_suggestion?: string; 615 | } 616 | 617 | Command { 618 | title?: string; 619 | hint?: string; 620 | icon?: string; 621 | value: string | string[]; 622 | bold?: boolean; 623 | italic?: boolean; 624 | align?: 'left'|'center'|'right'; 625 | selectable?: boolean; 626 | muted?: boolean; 627 | active?: integer; 628 | keep_open?: boolean; 629 | } 630 | ]] 631 | 632 | -- create_contengroups_menu 633 | function Mk_Playback:uosc_get_contengroups_menu() 634 | local menu = { -- init header 635 | type = "contengroups", 636 | title = "Matroska Content-Groups", 637 | items = {} 638 | } 639 | -- loop over the groups 640 | for i, cg in ipairs(self.content_groups) do 641 | table.insert(menu.items, {title = cg.name, value = "script-message-to mpvMatroska set-contentgroup " .. i --[[ this is now the command]]}) 642 | end 643 | 644 | local str = utils.format_json(menu) 645 | return str 646 | end 647 | 648 | -- create_editions_menu 649 | function Mk_Playback:uosc_get_editions_menu() 650 | local menu = { -- init header 651 | type = "intern-editions", 652 | title = "Editions", 653 | items = {} 654 | } 655 | -- loop over the internal editions 656 | for i, ie in ipairs(self.internal_editions) do 657 | if not ie.hidden then 658 | table.insert(menu.items, {title = ie.current_name, hint = mkp.get_timestamp(ie.duration, uosc_options.time_precision), 659 | active = i == self.current_edition_idx, 660 | value = "script-message-to mpvMatroska set-edition " .. i --[[ this is now the command]]}) 661 | end 662 | end 663 | 664 | local str = utils.format_json(menu) 665 | return str 666 | end 667 | 668 | 669 | 670 | -- private section ------------------------------------------------------------- 671 | 672 | -- close_files: a method to clean temp_files and mk_files 673 | function Mk_Playback:_close_files(only_temps) 674 | -- close temp_files 675 | for _, mk_file in ipairs(self.temp_files) do 676 | mk_file:close() 677 | end 678 | self.temp_files = {} 679 | -- exit if only temp_file 680 | if only_temps then return end 681 | 682 | -- close mk_files 683 | for _, mk_file in ipairs(self.mk_files) do 684 | if mk_file.seg_uuid ~= self.init_file.seg_uuid then 685 | mk_file:close() 686 | end 687 | end 688 | self.mk_files = {} 689 | end 690 | 691 | -- scan (private): scan the file and prepare all used Matroska features 692 | function Mk_Playback:_scan(path) 693 | self.init_dir = get_directory(path) 694 | 695 | self.init_file = mkp.Matroska_Parser:new(path) 696 | if not self.init_file.is_valid then 697 | return 698 | end 699 | 700 | self:_check_hard_linking() 701 | self:_analyze_chapters() 702 | self:_build_timelines() 703 | self:_check_content_grouping() 704 | end 705 | 706 | -- _get_main_file: returns the current main file 707 | function Mk_Playback:_get_main_file(for_tracks) 708 | -- for_tracks(boolean): not always are the tracks and the chapters in the same main file 709 | -- Hard-Linking: always the first file of mk_files 710 | if self.used_features[MK_FEATURE.hard_linking] then 711 | return self.mk_files[1] 712 | end 713 | 714 | -- linked-chapters files, init_file has only Chapters, no Tracks 715 | -- the main file depends on the chapers structure and can be any file in mk_files 716 | if for_tracks and self.used_features[MK_FEATURE.linked_chapters_file] then 717 | if self.current_edition_idx > 0 then 718 | local main_path = self.internal_editions[self.current_edition_idx]:get_main_filepath() 719 | for _, file in ipairs(self.mk_files) do 720 | if file.path == main_path then 721 | return file 722 | end 723 | end 724 | end 725 | end 726 | 727 | -- no specials 728 | return self.init_file 729 | end 730 | 731 | -- _analyze_chapters (private): analyze the chapters structure 732 | function Mk_Playback:_analyze_chapters() 733 | local mk_file = self:_get_main_file() 734 | self.available_chapters_langs = {} 735 | if mk_file == nil or mk_file.Chapters == nil then 736 | self.current_edition_idx = 1 737 | return 738 | end 739 | 740 | local edition, e, chap, c, is_ordered, display, d, lng, l, tag 741 | 742 | -- check linked-chapers-file 743 | if mk_file.Tracks == nil then 744 | self.used_features[MK_FEATURE.linked_chapters_file] = true 745 | end 746 | 747 | -- set default edition index 748 | edition, self.current_edition_idx = mk_file.Chapters:get_default_edition() 749 | 750 | -- check multiple editions, try to find a second edition 751 | if mk_file.Chapters:get_edition(2) then 752 | self.used_features[MK_FEATURE.multiple_editions] = true 753 | end 754 | 755 | -- process chapter 756 | local function process_chapter(chp, level) 757 | -- check linked-editions 758 | if not self.used_features[MK_FEATURE.linked_editions] then 759 | if chp:find_child(mk.chapters.ChapterSegmentEditionUID) then 760 | self.used_features[MK_FEATURE.linked_editions] = true 761 | end 762 | end 763 | 764 | -- check linked-chapters 765 | if not self.used_features[MK_FEATURE.linked_chapters] then 766 | if chp:find_child(mk.chapters.ChapterSegmentUUID) then 767 | self.used_features[MK_FEATURE.linked_chapters] = true 768 | end 769 | end 770 | 771 | -- check ChapProcess elements for menu features 772 | local c_process, cp = chp:find_child(mk.chapters.ChapProcess) 773 | while c_process do 774 | if not self.used_features[MK_FEATURE.native_menu] then 775 | if c_process:get_child(mk.chapters.ChapProcessCodecID).value == 0 then 776 | self.used_features[MK_FEATURE.native_menu] = true 777 | end 778 | end 779 | 780 | if not self.used_features[MK_FEATURE.dvd_menu] then 781 | if c_process:get_child(mk.chapters.ChapProcessCodecID).value == 1 then 782 | self.used_features[MK_FEATURE.dvd_menu] = true 783 | end 784 | end 785 | 786 | c_process, cp = chp:find_next_child(cp) 787 | end 788 | 789 | -- loop ChapterDisplays 790 | display, d = chp:find_child(mk.chapters.ChapterDisplay) 791 | while display do 792 | -- loop ChapLanguage 793 | lng, l = display:find_child(mk.chapters.ChapLanguage) 794 | while lng do 795 | self.available_chapters_langs[lng.value] = true -- save language 796 | lng, l = display:find_next_child(l) 797 | end 798 | 799 | -- loop ChapLanguageBCP47 800 | lng, l = display:find_child(mk.chapters.ChapLanguageBCP47) 801 | while lng do 802 | self.available_chapters_langs[lng.value] = true -- save language 803 | lng, l = display:find_next_child(l) 804 | end 805 | 806 | display, d = chp:find_next_child(d) 807 | -- check multiple chapter names 808 | if not self.used_features[MK_FEATURE.multiple_chapter_names] and display then 809 | self.used_features[MK_FEATURE.multiple_chapter_names] = true 810 | end 811 | end 812 | 813 | -- Hint: for the moment there is no search in the Tags 814 | 815 | -- check nested chapters 816 | if level > 8 then return end 817 | local nchap, nc = chp:find_child(mk.chapters.ChapterAtom) 818 | if nchap and level == 0 then -- check only at first level 819 | if is_ordered then 820 | self.used_features[MK_FEATURE.nested_ordered_chapters] = true 821 | else 822 | self.used_features[MK_FEATURE.nested_chapters] = true 823 | end 824 | end 825 | -- loop nested chapters 826 | while nchap do 827 | process_chapter(nchap, level + 1) 828 | nchap, nc = chp:find_next_child(nc) 829 | end 830 | end 831 | 832 | -- loop editions 833 | edition, e = mk_file.Chapters:find_child(mk.chapters.EditionEntry) 834 | while edition do 835 | is_ordered = false 836 | 837 | chap, c = edition:find_child(mk.chapters.ChapterAtom) 838 | -- check ordered chapters 839 | if chap and edition:get_child(mk.chapters.EditionFlagOrdered).value == 1 then 840 | self.used_features[MK_FEATURE.ordered_chapters] = true 841 | is_ordered = true 842 | end 843 | 844 | -- loop EditionDisplays 845 | display, d = edition:find_child(mk.chapters.EditionDisplay) 846 | while display do 847 | -- loop EditionLanguageIETF 848 | lng, l = display:find_child(mk.chapters.EditionLanguageIETF) 849 | while lng do 850 | self.available_chapters_langs[lng.value] = true -- save language 851 | lng, l = display:find_next_child(l) 852 | end 853 | 854 | display, d = edition:find_next_child(d) 855 | if display then self.used_features[MK_FEATURE.multiple_edition_names] = true end 856 | end 857 | 858 | -- Tags 859 | if mk_file.Tags then 860 | local counter = 0 861 | tag = mk_file.Tags:find_Tag_byName(edition, "TITLE") 862 | if tag then 863 | local simple, s = tag:find_child(mk.tags.SimpleTag) 864 | while simple do 865 | if simple:get_child(mk.tags.TagName).value == "TITLE" then 866 | lng = simple:get_child(mk.tags.TagLanguage).value 867 | self.available_chapters_langs[lng] = true -- save (old)language 868 | lng = simple:find_child(mk.tags.TagLanguageBCP47) 869 | if lng then 870 | self.available_chapters_langs[lng.value] = true -- save BCP47 language 871 | end 872 | if not self.used_features[MK_FEATURE.multiple_edition_names] then 873 | counter = counter + 1 874 | if counter > 1 then self.used_features[MK_FEATURE.multiple_edition_names] = true end 875 | end 876 | end 877 | 878 | simple, s = tag:find_next_child(s) 879 | end 880 | end 881 | end 882 | 883 | -- loop chapters 884 | while chap do 885 | process_chapter(chap, 0) 886 | chap, c = edition:find_next_child(c) 887 | end 888 | 889 | edition, e = mk_file.Chapters:find_next_child(e) 890 | end 891 | end 892 | 893 | -- _check_hard_linking (private): check and prepares everything 894 | function Mk_Playback:_check_hard_linking() 895 | -- check init file 896 | local used, sid, pid, nid = self.init_file:hardlinking_is_used() 897 | if not used then return end 898 | 899 | local paths = utils.readdir(self.init_dir, "files") 900 | if not paths then msg.error("Could not read directory '"..self.init_dir.."'") return end 901 | 902 | -- save the init prev and next UIDs 903 | local start_pid = pid 904 | local start_nid = nid 905 | self.temp_files = {} 906 | local segments = {} 907 | local mk_file 908 | self.mk_files = {} 909 | 910 | -- loop over the files 911 | for _, path in ipairs(paths) do 912 | path = self.init_dir..path 913 | -- check all paths, exclude the init_file 914 | if path ~= self.init_file.path then 915 | mk_file = mkp.Matroska_Parser:new(path) 916 | if mk_file.is_valid then 917 | sid, pid, nid = mk_file:hardlinking_get_uids() 918 | if sid ~= nil then 919 | table.insert(self.temp_files, mk_file) 920 | 921 | segments[sid] = { 922 | prev = pid, 923 | next = nid, 924 | idx = #self.temp_files 925 | } 926 | else 927 | mk_file:close() 928 | end 929 | end 930 | end 931 | end 932 | if #self.temp_files == 0 then return end 933 | 934 | table.insert(self.mk_files, self.init_file) 935 | 936 | -- endless loop check 937 | local function endless_loop(uid, start) 938 | -- it is possible(and easy) to build an endless loop with Hard-Linking 939 | -- for the prev-search we have to check all "left" files 940 | -- and for the next-search all "right" files 941 | -- the init_file is included in both search methods 942 | 943 | if start == nil then start = 1 end 944 | 945 | for i = start, #self.mk_files do 946 | if self.mk_files[i].seg_uuid == uid then return true end 947 | end 948 | return false 949 | end 950 | 951 | -- start backward search - prev ids 952 | if start_pid ~= nil then 953 | pid = start_pid 954 | 955 | while pid and segments[pid] do 956 | -- check endless looping 957 | if endless_loop(pid) then break end -- finish backward search 958 | -- insert in front 959 | table.insert(self.mk_files, 1, self.temp_files[segments[pid].idx]) 960 | pid = segments[pid].prev 961 | end 962 | 963 | -- finish backward search, the first mk_file is now the main file 964 | -- again are the Chapters important and can break Hard-Linking at this point, 965 | -- when the default edition of this first main file has ordered chapters 966 | if #self.mk_files > 1 and self.mk_files[1]:ordered_chapters_are_used() then 967 | self:_close_files() 968 | return 969 | end 970 | end 971 | 972 | -- start forward search - next ids 973 | if start_nid then 974 | local start = #self.mk_files -- start index -> is the init_file 975 | nid = start_nid 976 | 977 | while nid and segments[nid] do 978 | -- check endless looping 979 | if endless_loop(nid, start) then break end -- finish forward search 980 | -- append 981 | table.insert(self.mk_files, self.temp_files[segments[nid].idx]) 982 | nid = segments[nid].next 983 | end 984 | end 985 | 986 | -- check if another file was added 987 | if #self.mk_files == 1 then -- no file was added 988 | -- clean temp_ and mk_files 989 | self.mk_files = {} 990 | self:_close_files(true) 991 | return 992 | end 993 | 994 | -- Hard-Linking is used 995 | self.used_features[MK_FEATURE.hard_linking] = true 996 | end 997 | 998 | -- _build_timelines (private): generates for each edition a virtual chapter_timeline 999 | function Mk_Playback:_build_timelines() 1000 | self.internal_editions = {} 1001 | 1002 | local mk_file = self:_get_main_file() 1003 | if mk_file == nil then return end 1004 | 1005 | local intern_edition 1006 | local linked_editions -- a list of already all used linked edition uid's 1007 | 1008 | local run_time = 0 1009 | 1010 | -- load files, only needed for linked-chapters, some content is located in other files 1011 | if self.used_features[MK_FEATURE.linked_chapters] then 1012 | local files = utils.readdir(self.init_dir, "files") 1013 | if not files then msg.error("Mk_Playback:_build_timelines()", "Could not read directory '"..self.init_dir.."'") return end 1014 | 1015 | -- loop over the files 1016 | for _, file in ipairs(files) do 1017 | local path = self.init_dir .. file 1018 | -- check all paths, exclude the init_file 1019 | if path ~= mk_file.path then 1020 | local mkf = mkp.Matroska_Parser:new(path) 1021 | if mkf.is_valid and mkf.seg_uuid then 1022 | table.insert(self.temp_files, mkf) 1023 | end 1024 | end 1025 | end 1026 | end 1027 | 1028 | -- get linked file 1029 | local function get_linked_file(s_id) 1030 | -- search in mk_files first 1031 | for i, file in ipairs(self.mk_files) do 1032 | if file.seg_uuid == s_id then 1033 | return file 1034 | end 1035 | end 1036 | 1037 | -- search in temp_files 1038 | for i, file in ipairs(self.temp_files) do 1039 | if file.seg_uuid == s_id then 1040 | table.insert(self.mk_files, file) 1041 | table.remove(self.temp_files, i) 1042 | return file 1043 | end 1044 | end 1045 | 1046 | -- check init_file 1047 | if self.init_file.seg_uuid == s_id then 1048 | return self.init_file 1049 | end 1050 | return nil 1051 | end 1052 | 1053 | 1054 | -- process edition 1055 | local function process_edition(ed, nested_level, file) 1056 | -- prev_start_time: this var is used for non-ordered editions 1057 | -- without an end time it is not possible to calculate the duration 1058 | -- the duration of a chapter must be calculated from a next chapter's start time 1059 | -- for the last chapter is the duration calculated from the video(/file) duration 1060 | local prev_start_time = 0 1061 | 1062 | -- check edition is hidden 1063 | local ed_is_hidden = ed:is_hidden() 1064 | -- check ordered edition 1065 | local ed_is_ordered = ed:is_ordered() 1066 | -- first_chap: boolean, used for non-ordered editions 1067 | local first_chap = true 1068 | -- added_chap_idx: integer, index of the chapter segment, used for non-ordered editions 1069 | local added_chap_idx 1070 | 1071 | -- process chapter 1072 | local function process_chapter(chp, level) 1073 | -- no process for disabled chapters, for the moment 1074 | if chp:get_child(mk.chapters.ChapterFlagEnabled).value == 0 then return end 1075 | 1076 | -- start time 1077 | local start_time = chp:get_child(mk.chapters.ChapterTimeStart).value 1078 | 1079 | -- ordered chapter 1080 | if ed_is_ordered then 1081 | -- end time 1082 | local end_time = chp:find_child(mk.chapters.ChapterTimeEnd) 1083 | if end_time then end_time = end_time.value else end_time = 0 end 1084 | 1085 | -- linked-chapter or linked-edition 1086 | local c_seg_id = chp:find_child(mk.chapters.ChapterSegmentUUID) 1087 | if c_seg_id then 1088 | c_seg_id = mkp.Matroska_Parser:_bin2hex(c_seg_id.value) 1089 | -- try to find the linked file 1090 | local linked_file = get_linked_file(c_seg_id) 1091 | 1092 | -- no linked file found 1093 | if not linked_file then return end -- this skips also all nested chapters of this chapter 1094 | 1095 | -- linked-Edition 1096 | local c_seg_edition_id = chp:find_child(mk.chapters.ChapterSegmentEditionUID) 1097 | if c_seg_edition_id then 1098 | --TODO: no chapters in the linked file 1099 | if linked_file.Chapters == nil then 1100 | -- no chapters no editions 1101 | -- to skip this linked chapter is simple but I think it's better to use the entire file 1102 | -- add a virtual chapter for that 1103 | return -- skip fully for the moment 1104 | end 1105 | 1106 | c_seg_edition_id = c_seg_edition_id.value 1107 | 1108 | -- check if the linked Edition exists in the linked file 1109 | local linked_edition = linked_file.Chapters:get_edition(nil, c_seg_edition_id) 1110 | if not linked_edition then return end -- invalid linking 1111 | 1112 | -- check endless loop, Edition UID is already in the list 1113 | if linked_editions[c_seg_edition_id] then return end 1114 | 1115 | -- process the linked edition 1116 | linked_editions[c_seg_edition_id] = true -- add this edition uid 1117 | process_edition(linked_edition, level, linked_file) 1118 | 1119 | else -- linked-chapter, use chapter duration 1120 | -- add chapter segment 1121 | intern_edition:add_chapter_segment(chp, linked_file.path, run_time, level, ed_is_hidden) 1122 | -- increase runtime 1123 | if end_time > start_time then 1124 | run_time = run_time + (end_time - start_time) 1125 | end 1126 | end 1127 | 1128 | 1129 | else -- no ChapterSegmentUUID -> normal ordered chapter 1130 | -- add chapter segment 1131 | intern_edition:add_chapter_segment(chp, file.path, run_time, level, ed_is_hidden) 1132 | -- increase runtime 1133 | if end_time > start_time then 1134 | run_time = run_time + (end_time - start_time) 1135 | end 1136 | end 1137 | 1138 | 1139 | else -- non-ordered chapter 1140 | -- the run_time must be increased befor the chapter_segment is insert 1141 | 1142 | -- increase run_time 1143 | if start_time > prev_start_time then 1144 | run_time = run_time + (start_time - prev_start_time) 1145 | prev_start_time = start_time -- new prev start time 1146 | end 1147 | -- add a chapter segment 1148 | added_chap_idx = intern_edition:add_chapter_segment(chp, file.path, run_time, level, ed_is_hidden) 1149 | 1150 | -- set an end time for the prev chapter segment, start after first chapter 1151 | if not first_chap then 1152 | intern_edition:add_chap_endtime(added_chap_idx -1, start_time) 1153 | end 1154 | end 1155 | 1156 | -- nested chapters 1157 | if level > 8 then return end 1158 | local nchap, nc = chp:find_child(mk.chapters.ChapterAtom) 1159 | while nchap do 1160 | process_chapter(nchap, level + 1) 1161 | nchap, nc = chp:find_next_child(nc) 1162 | end 1163 | end 1164 | 1165 | 1166 | -- find first chapter 1167 | local chap, c = ed:find_child(mk.chapters.ChapterAtom) 1168 | 1169 | -- check first chapter 1170 | if chap then 1171 | -- check start time for non-ordered-editions 1172 | if not ed_is_ordered then 1173 | local stime = chap:get_child(mk.chapters.ChapterTimeStart).value 1174 | -- when stime is not 0 then add a virtual chapter in the chapter_timeline 1175 | if stime > 0 then 1176 | intern_edition:add_virtual_segment(file.path, run_time, nested_level) 1177 | end 1178 | end 1179 | 1180 | else -- there is no chapter in the edition 1181 | -- add a virtual segment of the entire video duration 1182 | local duration = file:get_video_duration() or file.seg_duration 1183 | intern_edition:add_virtual_segment(file.path, run_time, nested_level, 0, duration) 1184 | run_time = run_time + duration -- increase run_time 1185 | return 1186 | end 1187 | 1188 | -- loop chapters 1189 | while chap do 1190 | process_chapter(chap, nested_level) 1191 | first_chap = false 1192 | chap, c = ed:find_next_child(c) 1193 | end 1194 | 1195 | -- non-ordered editions, increase the run_time using the video duration to get the duration for the last chapter 1196 | if not ed_is_ordered then 1197 | local duration = file:get_video_duration() or file.seg_duration 1198 | if duration > prev_start_time then 1199 | run_time = run_time + (duration - prev_start_time) 1200 | end 1201 | 1202 | -- set an end time for the last chapter 1203 | intern_edition:add_chap_endtime(added_chap_idx, duration) 1204 | end 1205 | 1206 | end 1207 | 1208 | 1209 | -- edition_idx, an index of an edition in the Matroksa file 1210 | local edition_idx = 0 -- this value is needed for Hard-Linking to find the correct edition in other files 1211 | 1212 | -- process hard-linked files 1213 | local function process_hard_linked_files() 1214 | -- found edition: boolean, true when the edition could be found 1215 | local found_edition 1216 | 1217 | -- process all files exclude the first one 1218 | for i = 2, #self.mk_files do 1219 | found_edition = false 1220 | 1221 | -- check if chapters are present 1222 | if self.mk_files[i].Chapters then 1223 | local ed = self.mk_files[i].Chapters:get_edition(edition_idx) 1224 | if ed and not ed:is_ordered() then -- no ordered-chapter for the moment 1225 | -- only one scenario where ordered chapters are fine when the times are seamless until the video duration ends 1226 | 1227 | found_edition = true 1228 | process_edition(ed, 0, self.mk_files[i]) 1229 | end 1230 | end 1231 | 1232 | -- no chapters in the file or not the correct edition index or the edition is ordered 1233 | if not found_edition then 1234 | -- add a virtual segment of the entire video duration 1235 | local duration = self.mk_files[i]:get_video_duration() or self.mk_files[i].seg_duration 1236 | intern_edition:add_virtual_segment(self.mk_files[i].path, run_time, 0, 0, duration) 1237 | run_time = run_time + duration -- increase run_time 1238 | end 1239 | end 1240 | end 1241 | 1242 | 1243 | local edition, e 1244 | 1245 | -- check Chapters and find first edition 1246 | if mk_file.Chapters then 1247 | edition, e = mk_file.Chapters:find_child(mk.chapters.EditionEntry) 1248 | 1249 | else -- no Chapters in the file 1250 | 1251 | -- create a internal edition with a virtual segment of the file 1252 | intern_edition = Internal_Edition:new() 1253 | local duration = mk_file:get_video_duration() or mk_file.seg_duration 1254 | intern_edition:add_virtual_segment(mk_file.path, run_time, 0, 0, duration) 1255 | run_time = run_time + duration -- increase run_time 1256 | 1257 | -- check for Hard-Linking 1258 | if self.used_features[MK_FEATURE.hard_linking] then 1259 | edition_idx = 1 1260 | -- process all hard-linked file editions 1261 | process_hard_linked_files() 1262 | -- set intern_edition ordered to true, this is needed to generate the EDL path correctly 1263 | intern_edition.ordered = true 1264 | end 1265 | 1266 | -- save the run_time 1267 | intern_edition.duration = run_time 1268 | -- create edl_path 1269 | intern_edition:create_edl_path() 1270 | -- add internal edition 1271 | table.insert(self.internal_editions, intern_edition) 1272 | end 1273 | 1274 | -- loop main file editions 1275 | while edition do 1276 | run_time = 0 1277 | edition_idx = edition_idx + 1 1278 | intern_edition = Internal_Edition:new() 1279 | intern_edition.ordered = edition:is_ordered() 1280 | intern_edition.hidden = edition:is_hidden() 1281 | 1282 | -- ordered edition 1283 | if intern_edition.ordered then 1284 | linked_editions = {} -- init empty array 1285 | local uid = edition:find_child(mk.chapters.EditionUID) 1286 | if uid then 1287 | linked_editions[uid.value] = true -- add own UID to prevent endless looping 1288 | end 1289 | end 1290 | 1291 | -- check for Hard-Linking 1292 | if self.used_features[MK_FEATURE.hard_linking] then 1293 | -- no ordered editions in the main file allowed 1294 | if intern_edition.ordered then 1295 | -- add a virtual segment of the entire video duration 1296 | local duration = mk_file:get_video_duration() or mk_file.seg_duration 1297 | intern_edition:add_virtual_segment(mk_file.path, run_time, 0, 0, duration) 1298 | run_time = run_time + duration -- increase run_time 1299 | 1300 | else -- non-ordered edition 1301 | process_edition(edition, 0, mk_file) -- process the main edition 1302 | end 1303 | 1304 | -- process all hard-linked file editions 1305 | process_hard_linked_files() 1306 | -- set intern_edition ordered to true, this is needed to generate the EDL path correctly 1307 | intern_edition.ordered = true 1308 | 1309 | 1310 | else -- no Hard-Linking 1311 | process_edition(edition, 0, mk_file) 1312 | end 1313 | 1314 | -- save the run_time 1315 | intern_edition.duration = run_time 1316 | -- create edl_path 1317 | intern_edition:create_edl_path() 1318 | -- add internal edition 1319 | table.insert(self.internal_editions, intern_edition) 1320 | 1321 | edition, e = mk_file.Chapters:find_next_child(e) 1322 | end 1323 | 1324 | -- set active edl_path 1325 | self.edl_path = self.internal_editions[self.current_edition_idx].edl_path 1326 | end 1327 | 1328 | -- _check_content_grouping (private): a method to get the content groups 1329 | function Mk_Playback:_check_content_grouping() 1330 | -- content grouping is currently not in the Matroska specs 1331 | -- only a non-official way was implemented in HAALI Splitter called TRACKSETEX 1332 | -- TRACKSETEX is stored in the Matroska Tags 1333 | -- note: all index values are 0-based 1334 | 1335 | local mk_file = self:_get_main_file() 1336 | if not mk_file or not mk_file.Tags then return end 1337 | 1338 | -- trk_file: is mostly the same like mk_file but sometime not 1339 | local trk_file = self:_get_main_file(true) 1340 | if not trk_file then return end 1341 | 1342 | -- find the Tags with TRACKSETEX 1343 | local tag = mk_file.Tags:find_Tag_byName(nil, "TRACKSETEX") 1344 | if not tag then return end 1345 | 1346 | 1347 | -- process TRACKSETEX 1348 | local function process_TRACKSETEX(str) 1349 | if not str then return end 1350 | -- str example: "1 #0 #0 #2 eng English: 1.Edition with forced subs" 1351 | -- the first param could also an index start with "#" (an extension for TRACKSETEX) 1352 | -- the language param could also be extended for the BCP47 1353 | -- if a param is an UID then we need the main_track_file to check the track UIDs 1354 | local eid, vid, aid, sid, lng, name = str:match("^(%S+) (%S+) (%S+) (%S+) (%S+) ?(.*)$") 1355 | 1356 | -- check edition 1357 | if eid ~= "." and mk_file.Chapters then -- edition is defined 1358 | if eid:sub(1, 1) == "#" then -- edition index is used 1359 | eid = tonumber(eid:sub(2)) 1360 | if not eid then return end 1361 | eid = eid + 1 -- increse index to stay in synch with the internal editions 1362 | if eid > #self.internal_editions then return end 1363 | 1364 | else -- EditionUID is used 1365 | local ed, idx = mk_file.Chapters:get_edition(0, tonumber(eid)) 1366 | if ed then eid = idx else return end 1367 | end 1368 | 1369 | else -- eid is "." -> nil it 1370 | eid = nil 1371 | end 1372 | 1373 | -- check video 1374 | if vid == "." then vid = nil -- no video change 1375 | elseif vid == "x" then vid = 0 -- no video track 1376 | elseif vid:sub(1, 1) == "#" then -- video index is used 1377 | vid = tonumber(vid:sub(2)) 1378 | if not vid then return end 1379 | vid = vid + 1 -- increse index to stay in synch with the internal editions 1380 | -- check index 1381 | if not trk_file:get_video(vid) then return end 1382 | 1383 | else -- video UID is used 1384 | local trk, idx = trk_file:get_video(0, vid) 1385 | if trk then vid = idx end 1386 | end 1387 | 1388 | -- check audio 1389 | if aid == "." then aid = nil -- no audio change 1390 | elseif aid == "x" then aid = 0 -- no audio track 1391 | elseif aid:sub(1, 1) == "#" then -- audio index is used 1392 | aid = tonumber(aid:sub(2)) 1393 | if not aid then return end 1394 | aid = aid + 1 -- increse index to stay in synch with the internal editions 1395 | -- check index 1396 | if not trk_file:get_audio(aid) then return end 1397 | 1398 | else -- audio UID is used 1399 | local trk, idx = trk_file:get_audio(0, aid) 1400 | if trk then aid = idx end 1401 | end 1402 | 1403 | -- check subtitle 1404 | if sid == "." then sid = nil -- no subtitle change 1405 | elseif sid == "x" then sid = 0 -- no subtitle track 1406 | elseif sid:sub(1, 1) == "#" then -- subtitle index is used 1407 | sid = tonumber(sid:sub(2)) 1408 | if not sid then return end 1409 | sid = sid + 1 -- increse index to stay in synch with the internal editions 1410 | -- check index 1411 | if not trk_file:get_subtitle(sid) then return end 1412 | 1413 | else -- subtitle UID is used 1414 | local trk, idx = trk_file:get_subtitle(0, sid) 1415 | if trk then sid = idx end 1416 | end 1417 | 1418 | -- add content group 1419 | table.insert(self.content_groups, {name = name, vid = vid, aid = aid, sid = sid, eid = eid}) 1420 | end 1421 | 1422 | 1423 | -- loop SimpleTag 1424 | local simple, s = tag:find_child(mk.tags.SimpleTag) 1425 | while simple do 1426 | if simple:get_child(mk.tags.TagName).value == "TRACKSETEX" then 1427 | process_TRACKSETEX(simple:get_string()) 1428 | end 1429 | simple, s = tag:find_next_child(s) 1430 | end 1431 | 1432 | -- check the count of the groups 1433 | if #self.content_groups > 0 then 1434 | self.used_features[MK_FEATURE.haali_TRACKSETEX] = true 1435 | end 1436 | end 1437 | 1438 | -- _set_content_from_group (private): sets the defined content from a content group 1439 | function Mk_Playback:_set_content_from_group(idx) 1440 | if idx == nil then idx = 1 end 1441 | if idx > #self.content_groups then return end 1442 | 1443 | self.content_group_idx = idx 1444 | local cont = self.content_groups[idx] 1445 | 1446 | -- video 1447 | if cont.vid ~= nil then 1448 | if cont.vid == 0 then -- no video 1449 | mp.set_property_native("vid", "no") 1450 | else 1451 | mp.set_property_native("vid", cont.vid) 1452 | end 1453 | end 1454 | 1455 | -- audio 1456 | if cont.aid ~= nil then 1457 | if cont.aid == 0 then -- no audio 1458 | mp.set_property_native("aid", "no") 1459 | else 1460 | mp.set_property_native("aid", cont.aid) 1461 | end 1462 | end 1463 | 1464 | -- sub 1465 | if cont.sid ~= nil then 1466 | if cont.sid == 0 then -- no sub 1467 | mp.set_property_native("sid", "no") 1468 | else 1469 | mp.set_property_native("sid", cont.sid) 1470 | end 1471 | end 1472 | 1473 | -- edition 1474 | if cont.eid then 1475 | self:edition_changing(cont.eid) 1476 | return 1477 | end 1478 | 1479 | mp.osd_message(cont.name) 1480 | end 1481 | 1482 | -- _set_edition_names (private): a method to change the edition depend on the given languages 1483 | function Mk_Playback:_set_edition_names(langs) 1484 | -- langs: array with language codes, when nil the first name is used 1485 | if langs == nil then langs = {""} end -- no language code -> first name 1486 | 1487 | local c_name 1488 | local mk_file = self:_get_main_file() 1489 | if not mk_file then return end 1490 | 1491 | -- loop of the internal editions 1492 | for i = 1, #self.internal_editions do 1493 | c_name = "" 1494 | 1495 | -- find edition name 1496 | for _, lng in ipairs(langs) do 1497 | -- check lng, nothing todo if the current_lang match 1498 | if lng == self.internal_editions[i].current_lang then break end 1499 | 1500 | c_name = mk_file:get_edition_name(i, lng, false, true) 1501 | 1502 | -- name found 1503 | if c_name ~= "" then 1504 | self.internal_editions[i].current_name = c_name 1505 | self.internal_editions[i].current_lang = lng -- set new language 1506 | break 1507 | end 1508 | end 1509 | 1510 | -- no new name found and current_name is empty 1511 | if c_name == "" and self.internal_editions[i].current_name == "" then 1512 | c_name = mk_file:get_edition_name(i, "") -- get first edition name 1513 | -- check again, name could be empty -> no chapter Tag/Display or a really empty name 1514 | if c_name == "" then c_name = "Edition " .. i end 1515 | self.internal_editions[i].current_name = c_name 1516 | self.internal_editions[i].current_lang = "" 1517 | end 1518 | 1519 | end 1520 | end 1521 | 1522 | -- _change_edition_chapter_names (private): set new names 1523 | function Mk_Playback:_change_edition_chapter_names(pref_sub_lang) 1524 | -- check multiple chapter/edition names 1525 | if self.used_features[MK_FEATURE.multiple_chapter_names] 1526 | or self.used_features[MK_FEATURE.multiple_edition_names] then 1527 | -- set new chapter and edition names 1528 | local c_list = self:get_mpv_chapters(false, pref_sub_lang) 1529 | if c_list then 1530 | mp.set_property_native("chapter-list", c_list) 1531 | end 1532 | end 1533 | 1534 | -- set new media title 1535 | if self.used_features[MK_FEATURE.multiple_edition_names] then 1536 | self:mpv_set_media_title() 1537 | end 1538 | end 1539 | 1540 | 1541 | 1542 | -- find chapter by UID (private): returns an internal chapter segment 1543 | function Mk_Playback:_find_chapter_byUID(uid) 1544 | -- this method will be needed for the GotoAndPlay() command 1545 | local intern_chap = nil 1546 | 1547 | for i, intern_edition in ipairs(self.internal_editions) do 1548 | intern_chap = intern_edition:find_chapter_byUID(uid) 1549 | 1550 | if intern_chap then break end 1551 | end 1552 | 1553 | return intern_chap 1554 | end 1555 | 1556 | 1557 | -- _chapter_process_handler (private): a method for handling a chapter and the commands 1558 | function Mk_Playback:_chapter_process_handler(intern_chap, process_time) 1559 | --if intern_chap == nil then return end 1560 | local chap = intern_chap.Chapter 1561 | local process, p = chap:find_child(mk.chapters.ChapProcess) 1562 | 1563 | if process == nil then return end 1564 | 1565 | local function process_commands() 1566 | local cmd, c = process:find_child(mk.chapters.ChapProcessCommand) 1567 | if cmd == nil then return end 1568 | 1569 | -- loop over the commands 1570 | while cmd do 1571 | -- check process time 1572 | local ptime = cmd:get_child(mk.chapters.ChapProcessTime).value 1573 | if ptime == process_time or ptime == mk.CHAP_PROCESS_TIME.DURING then 1574 | self:_MatroskaScript_Interpreter(cmd:get_child(mk.chapters.ChapProcessData).value) 1575 | end 1576 | 1577 | cmd, c = process:find_next_child(c) 1578 | end 1579 | end 1580 | 1581 | -- loop over the processes 1582 | while process do 1583 | if process:get_child(mk.chapters.ChapProcessCodecID).value == 0 then -- Matroska Scripts 1584 | process_commands() 1585 | end 1586 | 1587 | process, p = chap:find_next_child(p) 1588 | end 1589 | 1590 | end 1591 | 1592 | -- _MatroskaScript_Interpreter (private): Interpret the commands 1593 | function Mk_Playback:_MatroskaScript_Interpreter(cmd_data) 1594 | --[[ GotoAndPlay( ChapterUID ); for the moment the only existing command 1595 | planned commands: Add Interactive Movie MatroskaScript commands #658 (GitHub issue) 1596 | 1597 | choice_variable = CreateChoice(); 1598 | choice_variable.SetText('some text', ); 1599 | choice_variable.SetOnSelected( ); 1600 | MakeDefaultChoice(a_choice_variable); 1601 | 1602 | ]] 1603 | 1604 | local script, value = cmd_data:match("(.+)%((.*)%);$") 1605 | 1606 | if script == "GotoAndPlay" then 1607 | local chap = self:_find_chapter_byUID(tonumber(value)) 1608 | if chap then 1609 | mp.set_property_number("playback-time", chap.timeline_marker / 1e9) 1610 | end 1611 | end 1612 | end 1613 | 1614 | 1615 | 1616 | -- export -- 1617 | return { 1618 | Mk_Playback = Mk_Playback 1619 | } 1620 | -------------------------------------------------------------------------------- /src/mpvmatroska.lua: -------------------------------------------------------------------------------- 1 | local mp = require "mp" 2 | local msg = require "mp.msg" 3 | opt = require('mp.options') 4 | 5 | local mkplayback = require "matroskaplayback" 6 | local mkplay 7 | 8 | 9 | 10 | -- disable mpv's internal ordered-chapters support! 11 | mp.set_property_native("ordered-chapters", false) 12 | 13 | -- uosc support for better handling menus and buttons 14 | local uosc_is_installed = false 15 | uosc_options = { 16 | time_precision = 0, 17 | } 18 | 19 | -- Register response handler 20 | mp.register_script_message('uosc-version', function(version) 21 | uosc_is_installed = version ~= nil 22 | end) 23 | 24 | 25 | 26 | local function mp_file_loaded() 27 | -- get the stream id's 28 | mkplay.mpv_current_vid = mp.get_property_number("vid") 29 | mkplay.mpv_current_aid = mp.get_property_number("aid") 30 | mkplay.mpv_current_sid = mp.get_property_number("sid") 31 | 32 | -- check video rotation 33 | mkplay:video_rotation() 34 | 35 | -- init chapters 36 | mp.set_property_native("chapter-list", mkplay:get_mpv_chapters(true)) 37 | msg.debug("Matroska Playback: init chapters done") 38 | 39 | -- set media title 40 | mkplay:mpv_set_media_title() 41 | 42 | -- check edition change 43 | if mkplay.edition_is_changing then 44 | mkplay.edition_is_changing = false -- end of edition changing 45 | mp.osd_message(mkplay:current_edition().current_name) 46 | return 47 | end 48 | 49 | -- register video observation 50 | mp.observe_property("vid", "number", function(_, val) mkplay:mpv_on_video_change(val) end) 51 | -- register audio observation 52 | mp.observe_property("aid", "number", function(_, val) mkplay:mpv_on_audio_change(val) end) 53 | -- register subtitle observation 54 | mp.observe_property("sid", "number", function(_, val) mkplay:mpv_on_subtitle_change(val) end) 55 | 56 | -- key binding: cycle-editions, override mpv's default "E" key for changing the editions 57 | mp.add_key_binding("E", "cycle-editions", function() mkplay:edition_changing(0) end) 58 | 59 | -- key binding: cycle content groups 60 | mp.add_key_binding("g", "cycle-contentgroups", function() mkplay:cycle_content_groups() end) 61 | 62 | 63 | -- register playback-time observation: only for Matroska Native menu 64 | if mkplay.used_features[MK_FEATURE.native_menu] then 65 | mp.observe_property("playback-time", "number", function(_, val) mkplay:observe_playback_time(val) end) 66 | end 67 | end 68 | 69 | 70 | local function mp_on_load() 71 | msg.info("Matroska Playback: on_load") 72 | -- mkplay is already running 73 | if mkplay then 74 | -- check edition change 75 | if mkplay.edition_is_changing then return end 76 | 77 | -- close the current instance 78 | mkplay:close() 79 | end 80 | 81 | mkplay = mkplayback.Mk_Playback:new(mp.get_property("stream-open-filename", "")) 82 | 83 | -- when the edl_path is not empty then Matroska features are present and mpvMatroska is used 84 | if mkplay.edl_path ~= "" then 85 | mp.register_event("file-loaded", mp_file_loaded) 86 | mp.set_property("stream-open-filename", mkplay.edl_path) 87 | 88 | mp.register_script_message("cycle-editions", function() mkplay:edition_changing(0) end) 89 | mp.register_script_message("set-edition", function(idx) mkplay:edition_changing(tonumber(idx)) end) 90 | mp.register_script_message("cycle-contentgroups", function() mkplay:cycle_content_groups() end) 91 | mp.register_script_message("set-contentgroup", function(idx) mkplay:_set_content_from_group(tonumber(idx)) end) 92 | 93 | -- uosc settings 94 | if uosc_is_installed then 95 | opt.read_options(uosc_options, 'uosc') 96 | 97 | mp.register_script_message("open-editions", 98 | function() mp.commandv('script-message-to', 'uosc', 'open-menu', mkplay:uosc_get_editions_menu()) end) 99 | mp.add_key_binding("ALT+e", "open-editions", 100 | function() mp.commandv('script-message-to', 'uosc', 'open-menu', mkplay:uosc_get_editions_menu()) end) 101 | 102 | mp.register_script_message("open-contentgroups", 103 | function() mp.commandv('script-message-to', 'uosc', 'open-menu', mkplay:uosc_get_contengroups_menu()) end) 104 | mp.add_key_binding("ALT+g", "open-contentgroups", 105 | function() mp.commandv('script-message-to', 'uosc', 'open-menu', mkplay:uosc_get_contengroups_menu()) end) 106 | end 107 | 108 | -- the loaded file is not Matroska, disable mpvMatroska 109 | else 110 | mkplay:close() 111 | mkplay = nil 112 | end 113 | end 114 | 115 | 116 | 117 | -- hooks 118 | mp.add_hook("on_load", 50, mp_on_load) 119 | --mp.add_hook("on_preloaded", 50, mp_on_preloaded) 120 | 121 | --------------------------------------------------------------------------------