diff options
| author | Vito Graffagnino <vito@graffagnino.xyz> | 2020-09-08 18:10:49 +0100 |
|---|---|---|
| committer | Vito Graffagnino <vito@graffagnino.xyz> | 2020-09-08 18:10:49 +0100 |
| commit | 3b0142cedcde39e4c2097ecd916a870a3ced5ec6 (patch) | |
| tree | 2116c49a845dfc0945778f2aa3e2118d72be428b /.config/mpv/scripts/interactive-video.lua | |
| parent | 8cc927e930d5b6aafe3e9862a61e81705479a1b4 (diff) | |
Added the relevent parts of the .config directory. Alss add ssh config
Diffstat (limited to '.config/mpv/scripts/interactive-video.lua')
| -rw-r--r-- | .config/mpv/scripts/interactive-video.lua | 905 |
1 files changed, 905 insertions, 0 deletions
diff --git a/.config/mpv/scripts/interactive-video.lua b/.config/mpv/scripts/interactive-video.lua new file mode 100644 index 0000000..dff046a --- /dev/null +++ b/.config/mpv/scripts/interactive-video.lua @@ -0,0 +1,905 @@ +local utils = require "mp.utils" +local msg = require "mp.msg" + + +--[[ Utility functions ]]------------------------------------------------------ + +-- Change this to `msg.info' to see debug messages on mpv output. +local msg_debug = msg.debug + +-- Count elements in a table. +function count(tbl) + local n = 0 + for _ in pairs(tbl) do n = n + 1 end + return n +end + +-- Make a copy a table (but not of its elements). +function shallow_copy(orig) + local copy + if type(orig) == "table" then + copy = {} + for key, val in pairs(orig) do + copy[key] = val + end + else + copy = orig + end + return copy +end + +-- Round to nearest integer. +math.round = function(x) + return math.floor(x + 0.5) +end + + +--[[ Time functions ]]--------------------------------------------------------- + +-- Only used for rounding purposes: having the exact value is not necessary. +-- Default to 25fps (i.e., 40ms). +local frame_duration = 40 + +-- Get frame duration from container-fps property. +function update_frame_duration() + local fps = mp.get_property_number("container-fps") or 25 + frame_duration = 1000 / (fps > 0 and fps or 25) +end +mp.register_event("file-loaded", update_frame_duration) + +-- Round time position (in ms) to a multiple of frame_duration. +function round_to_frame(time) + return math.round(math.round(time / frame_duration) * frame_duration) +end + +-- Time position of previous frame. +function prev_frame(time) + return round_to_frame(time - frame_duration) +end + +-- Format time position (in ms) as hh:mm:ss.msc. +function time_to_string(time) + return string.format("%02d:%02d:%06.3f", + math.floor(time / 3600000), + math.floor(time / 60000) % 60, + (time % 60000) / 1000) +end + +-- Retrieve the time-pos property (in ms) and round it to a frame position. +function get_time_pos() + local t = (mp.get_property_number("time-pos") or 0) * 1000 - frame_duration + return round_to_frame(t) +end + +-- Seek playback to given time position (in ms). +function set_time_pos(time) + msg_debug("seek to: " .. time_to_string(time)) + local t = round_to_frame(time + frame_duration) / 1000 + mp.set_property_number("time-pos", t) +end + +-- Retrieve the duration property (in ms) and round it to a frame position. +function get_duration() + local t = (mp.get_property_number("duration") or 0) * 1000 - frame_duration + return round_to_frame(t) +end + + +--[[ Global data and state variables ]]---------------------------------------- + +local initial_segment, segments, moments, preconditions, segment_groups +local state = { active = false } + + +--[[ Apply impression ]]------------------------------------------------------- + +function apply_impression(impression) + if impression == nil then + return + + elseif impression.type == "userState" then + for var, val in pairs(impression.data.persistent or {}) do + msg_debug("set variable: " .. var .. " = " .. + utils.to_string(val) .. " (was: " .. + utils.to_string(state.vars[var]) .. ")") + state.vars[var] = val + end + + else + msg.error("Invalid type of impression data: " .. + utils.to_string(impression.type)) + end +end + + +--[[ Evaluate precondition ]]-------------------------------------------------- + +function eval_precondition(cond) + if cond == nil then + return true + + elseif type(cond) ~= "table" then + return cond + + elseif #cond == 0 then + msg.error("Empty precondition expression") + + elseif cond[1] == "not" and #cond == 2 then + return not eval_precondition(cond[2]) + + elseif cond[1] == "and" then + for i = 2, #cond do + if not eval_precondition(cond[i]) then return false end + end + return true + + elseif cond[1] == "or" then + for i = 2, #cond do + if eval_precondition(cond[i]) then return true end + end + return false + + elseif cond[1] == "eql" and #cond == 3 then + return eval_precondition(cond[2]) == eval_precondition(cond[3]) + + elseif cond[1] == "persistentState" and #cond == 2 then + return state.vars[cond[2]] + + else + msg.error("Invalid precondition: " .. utils.to_string(cond)) + end + + return false +end + + +--[[ Format strings for displaying choices on OSD ]]--------------------------- + +function format_osd_choices(choices) + local osd_choices = {} + for i = 1, #choices do + local str = "" + for j, ch in ipairs(choices) do + local b = i == j + str = str .. "{\\fscx70\\fscy70\\an5" + .. "\\pos(" .. ((2*j-1) / (2*#choices)) .. ",0.048)}" + .. (i == j and "{\\c&HFFFFFF&}[ " or "{\\c&H7F7F7F&}") + .. ch.text + .. (i == j and " ]" or "") + .. "\n" + end + table.insert(osd_choices, str) + end + return osd_choices +end + + +--[[ Update OSD according to current state ]]---------------------------------- + +function update_osd() + -- Multiple-choice input. + if state.osd_choices and type(state.osd_input) == "number" then + mp.set_osd_ass(1, 1, state.osd_choices[state.osd_input]) + + -- Code entry input. + elseif state.osd_prompt and type(state.osd_input) == "string" then + mp.set_osd_ass(1, 1, "{\\fscx70\\fscy70\\an4\\pos(0.02,0.048)}" .. + state.osd_prompt .. " " .. state.osd_input .. "_\n") + + -- Nothing to display. + else + mp.set_osd_ass(0, 0, "") + end +end + + +--[[ Navigation and input control functions ]]--------------------------------- + +-- Dummy function. +function cmd_nop() +end + +-- Seek forward. +function cmd_fwd(sec, skip) + return function() + process_events(get_time_pos() + sec * 1000, { no_skip = not skip, + ffwd = true }) + end +end + +-- Seek backward. +function cmd_bwd(sec, skip) + return function() + rewind(sec * 1000, { no_skip = not skip }) + end +end + +-- Select next choice. +function cmd_input_next() + if state.osd_input < #state.osd_choices then + state.osd_input = state.osd_input + 1 + else + state.osd_input = 1 + end + update_osd() +end + +-- Select previous choice. +function cmd_input_prev() + if state.osd_input > 1 then + state.osd_input = state.osd_input - 1 + else + state.osd_input = #state.osd_choices + end + update_osd() +end + +-- Input character. +function cmd_input_char(char) + return function() + state.osd_input = state.osd_input .. char + update_osd() + end +end + +-- Delete last input character. +function cmd_input_backspace() + if string.len(state.osd_input) > 0 then + state.osd_input = string.sub(state.osd_input, 1, -2) + update_osd() + end +end + +-- Submit current input. +function cmd_input_submit() + state.osd_choices = nil + state.osd_prompt = nil + table.insert(state.events, 1, { time = get_time_pos(), type = "submit", + mom_id = state.osd_mom_id }) + update_osd() + update_controls() +end + + +--[[ Key mappings for control functions ]]------------------------------------- + +-- Merge key mappings. +-- Mappings in map1 take precedence over those in map2. +function mapping_merge(map1, map2) + for key, ctrl in pairs(map2) do + map1[key] = map1[key] or ctrl + end +end + +-- Key mapping for navigation. +local nav_mapping = { + ["RIGHT"] = { "iv-nav-right", cmd_fwd( 10), {repeatable=true} }, + ["SHIFT+RIGHT"] = { "iv-nav-sright", cmd_fwd( 10,true), {repeatable=true} }, + ["LEFT"] = { "iv-nav-left", cmd_bwd( 10), {repeatable=true} }, + ["SHIFT+LEFT"] = { "iv-nav-sleft", cmd_bwd( 10,true), {repeatable=true} }, + ["UP"] = { "iv-nav-up", cmd_fwd( 60), {repeatable=true} }, + ["SHIFT+UP"] = { "iv-nav-sup", cmd_fwd( 60,true), {repeatable=true} }, + ["DOWN"] = { "iv-nav-down", cmd_bwd( 60), {repeatable=true} }, + ["SHIFT+DOWN"] = { "iv-nav-sdown", cmd_bwd( 60,true), {repeatable=true} }, + ["PGUP"] = { "iv-nav-pgup", cmd_fwd(1/0), {repeatable=true} }, + ["SHIFT+PGUP"] = { "iv-nav-spgup", cmd_fwd(600,true), {repeatable=true} }, + ["PGDWN"] = { "iv-nav-pgdwn", cmd_bwd(600), {repeatable=true} }, + ["SHIFT+PGDWN"] = { "iv-nav-spgdwn", cmd_bwd(600,true), {repeatable=true} }, +} + +-- Control mapping for multiple-choice input. +local input_choice_mapping = { + ["RIGHT"] = { "iv-input-right", cmd_input_next, { repeatable = true } }, + ["LEFT"] = { "iv-input-left", cmd_input_prev, { repeatable = true } }, + ["ENTER"] = { "iv-input-enter", cmd_input_submit }, +} +for _, key in ipairs({ "UP", "DOWN", "PGUP", "PGDWN" }) do + input_choice_mapping[key] = { "iv-nop-"..key, cmd_nop } +end +mapping_merge(input_choice_mapping, nav_mapping) + +-- Control mapping for code entry input. +local input_code_mapping = { + ["BS"] = { "iv-input-bs", cmd_input_backspace, { repeatable = true } }, + ["ENTER"] = { "iv-input-enter", cmd_input_submit }, +} +for _, key in ipairs({ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }) do + input_code_mapping[key] = { "iv-input-" ..key, cmd_input_char(key) } + input_code_mapping["KP"..key] = { "iv-input-kp"..key, cmd_input_char(key) } +end +for _, key in ipairs({ "RIGHT", "LEFT", "UP", "DOWN", "PGUP", "PGDWN" }) do + input_code_mapping[key] = { "iv-nop-" ..key, cmd_nop } +end +mapping_merge(input_code_mapping, nav_mapping) + + +--[[ Update controls according to current state ]]----------------------------- + +local current_mapping + +function update_controls() + local new_mapping + + if state.active then + -- Multiple-choice input. + if state.osd_choices and type(state.osd_input) == "number" then + new_mapping = input_choice_mapping + + -- Code entry input. + elseif state.osd_prompt and type(state.osd_input) == "string" then + new_mapping = input_code_mapping + + -- No input: only navigation controls. + else + new_mapping = nav_mapping + end + end + + if new_mapping ~= current_mapping then + -- Remove current control bindings. + for key, ctrl in pairs(current_mapping or {}) do + mp.remove_key_binding(ctrl[1]) + end + + -- Register new control bindings. + current_mapping = new_mapping + for key, ctrl in pairs(current_mapping or {}) do + mp.add_forced_key_binding(key, table.unpack(ctrl)) + end + end +end + + +--[[ Deactivate interactive video playback ]]---------------------------------- + +function deactivate() + mp.unregister_event(on_tick) + state = { active = false } + update_osd() + update_controls() +end + + +--[[ Retrieve first valid segment from segment group ]]------------------------ + +function resolve_segment_group(group_id) + for _, seg_id in ipairs(segment_groups[group_id] or {}) do + if type(seg_id) == "table" and seg_id.segmentGroup then + seg_id = resolve_segment_group(seg_id.segmentGroup) + if seg_id then return seg_id end + else + local cond_id = seg_id + if type(seg_id) == "table" then + cond_id = seg_id.precondition + seg_id = seg_id.segment + end + if eval_precondition(preconditions[cond_id]) then + return seg_id + end + end + end + return nil +end + + +--[[ Load segment and schedule all related events in the event queue ]]-------- + +function load_segment(seg_id) + msg_debug("load segment: " .. seg_id) + state.seg_id = seg_id + + state.events = {} + + -- Add moment-related events. + for i, mom in ipairs(moments[state.seg_id] or {}) do + table.insert(state.events, { time = mom.startMs, + type = "start", mom_id = i }) + table.insert(state.events, { time = prev_frame(mom.endMs), + type = "end", mom_id = i }) + if string.sub(mom.type, 1, 6) == "scene:" then + table.insert(state.events, { time = mom.uiDisplayMS, + type = "display", mom_id = i }) + table.insert(state.events, { time = prev_frame(mom.uiHideMS), + type = "hide", mom_id = i }) + end + end + + -- Sort moment-related events. + table.sort(state.events, function(ev1, ev2) + if ev1.time ~= ev2.time or ev1.mom_id ~= ev2.mom_id then + return ev1.time < ev2.time + elseif ev1.type ~= ev2.type then + return ev1.type == "start" or ev2.type == "end" or + (ev1.type == "display" and ev2.type == "hide") + else + return false + end + end) + + -- Add segment start and end events. + local seg = segments[state.seg_id] + table.insert(state.events, 1, { time = seg.startTimeMs, + type = "start_seg" }) + table.insert(state.events, { time = prev_frame(seg.endTimeMs or + get_duration()), + type = "end_seg" }) +end + + +--[[ Process all pending events until given (or current) time position ]]------ + +function process_events(time, flags) + time = time or get_time_pos() + flags = flags or {} + + local seek = flags.ffwd + + if not state.events then return end + while #state.events > 0 and time >= state.events[1].time do + -- Pop first event from queue. + local ev = state.events[1] + table.remove(state.events, 1) + + msg_debug("process event: " .. time_to_string(ev.time) .. " " .. + string.format("%-10s", ev.type) .. + state.seg_id .. (ev.mom_id and ("/" .. ev.mom_id) or "") .. + ((ev.type == "submit" or ev.type == "end") and state.osd_input + and (" (" .. utils.to_string(state.osd_input) .. ")") or "")) + + local next_seg_id = nil + + -- Moment-related event? + if ev.mom_id then + local mom = moments[state.seg_id][ev.mom_id] + + -- Start of new moment? + if ev.type == "start" then + -- Check precondition, and remove all moments related to this event + -- if it is not satisfied. + if not eval_precondition(mom.precondition) then + for i = #state.events, 1, -1 do + if state.events[i].mom_id == ev.mom_id then + table.remove(state.events, i) + end + end + else + apply_impression(mom.impressionData) + end + + -- Scene-related event? + elseif string.sub(mom.type, 1, 6) == "scene:" then + -- Display user input controls. + if ev.type == "display" then + state.osd_mom_id = ev.mom_id + if mom.config.hasMultipleChoiceInput then + state.osd_prompt = "Enter code:" + state.osd_input = "" + else + state.osd_choices = format_osd_choices(mom.choices) + state.osd_input = mom.defaultChoiceIndex + 1 + end + if state.hist[state.hist_idx].inputs[ev.mom_id] then + state.osd_input = state.hist[state.hist_idx].inputs[ev.mom_id] + end + if flags.no_skip and time > ev.time then + time = ev.time + seek = true + end + + -- Hide user input controls. + elseif ev.type == "hide" then + state.osd_choices = nil + state.osd_prompt = nil + state.osd_mom_id = nil + + -- Select branch according to user input. + elseif ev.type == "end" or + (ev.type == "submit" and + not mom.config.disableImmediateSceneTransition) then + -- Record input in history. + -- Clear all forward history if input different from recorded one. + local hist = state.hist[state.hist_idx] + if state.osd_input ~= hist.inputs[ev.mom_id] then + hist.inputs[ev.mom_id] = state.osd_input + for i, mi in ipairs(moments[state.seg_id]) do + if string.sub(mi.type, 1, 6) == "scene:" and + mi.startMs >= mom.endMs then + hist.inputs[i] = nil + end + end + for i = #state.hist, state.hist_idx+1, -1 do + state.hist[i] = nil + end + end + + -- Find corresponding choice. + local choice + if mom.config.hasMultipleChoiceInput then + for _, ch in ipairs(mom.choices) do + if not ch.code or state.osd_input == ch.code then + choice = ch + break + end + end + if not choice then + msg.error("No choice available for input '" .. state.osd_input .. + "' in moment " .. state.seg_id .. "/" .. ev.mom_id) + deactivate() + return + end + else + choice = mom.choices[state.osd_input] + end + apply_impression(choice.impressionData) + + -- Select next segment accordingly. + if not (mom.trackingInfo and + mom.trackingInfo.optionType == "fakeOption") then + next_seg_id = choice.segmentId or resolve_segment_group(choice.sg) + if not next_seg_id then + msg.error("No segment for choice '" .. choice.id .. "' " .. + "of moment " .. state.seg_id .. "/" .. ev.mom_id) + deactivate() + return + end + end + state.osd_input = nil + end + end + + -- Start of current segment? + elseif ev.type == "start_seg" then + state.hist_idx = state.hist_idx + 1 + if not state.hist[state.hist_idx] then + state.hist[state.hist_idx] = { seg_id = state.seg_id, + vars = shallow_copy(state.vars), + inputs = {} } + end + + -- End of current segment? + elseif ev.type == "end_seg" then + next_seg_id = resolve_segment_group(state.seg_id) or + next(segments[state.seg_id].next) + if not next_seg_id then + msg.debug("No segment after " .. state.seg_id .. "; " .. + "assuming end of video") + deactivate() + mp.commandv("playlist-next", "force") + return + end + end + + -- Load next segment? + if next_seg_id then + local cur_seg = segments[state.seg_id] + local next_seg = segments[next_seg_id] + + -- If next segment does not directly follow current segment, + -- we need to jump. + if next_seg.startTimeMs ~= cur_seg.endTimeMs then + if flags.ffwd then + time = next_seg.startTimeMs + time - prev_frame(cur_seg.endTimeMs) + else + time = next_seg.startTimeMs + end + seek = true + + -- Also jump if skipping end of current segment. + elseif time < next_seg.startTimeMs then + time = next_seg.startTimeMs + seek = true + end + + load_segment(next_seg_id) + end + end + + -- Jump to current time position, if required. + if seek then + set_time_pos(time) + end + + -- Update OSD and controls according to current state. + update_osd() + update_controls() +end + + +--[[ Seek backwards and rewind history ]]-------------------------------------- + +function rewind(delay, flags) + flags = flags or {} + + if state.hist_idx == 0 then return end + + local time = get_time_pos() + local seg_id = state.hist[state.hist_idx].seg_id + local first = true + + -- Keep rewinding from segment to segment until delay is consumed. + while delay > 0 and (first or state.hist_idx > 1) do + if first then + first = false + else + state.hist_idx = state.hist_idx - 1 + seg_id = state.hist[state.hist_idx].seg_id + time = segments[seg_id].endTimeMs + end + + -- If in no-skip mode, find previous scene, if any. + if flags.no_skip then + local prev_mom + for _, mom in ipairs(moments[seg_id]) do + if string.sub(mom.type, 1, 6) == "scene:" and + time - delay <= mom.uiDisplayMS and mom.uiDisplayMS < time and + (not prev_mom or prev_mom.uiDisplayMS < mom.uiDisplayMS) then + prev_mom = mom + end + end + if prev_mom then + time = prev_mom.uiDisplayMS + delay = 0 + break + end + end + + -- Rewind until start of current segment. + delay = delay - time + segments[seg_id].startTimeMs + time = segments[seg_id].startTimeMs + end + + -- Adjust actual time. + if delay < 0 then + time = time - delay + end + + -- Load current segment and state variables from history. + load_segment(seg_id) + shallow_copy(state.vars, state.hist[state.hist_idx].vars) + state.hist_idx = state.hist_idx - 1 + state.osd_choices = nil + state.osd_prompt = nil + + -- Re-run current segment's history forward until actual time. + process_events(time, { ffwd = true }) +end + + +--[[ Load and check JSON data files, if any, on file load ]]------------------- + +function on_start_file(_) + deactivate() + + -- Look for JSON data files. + + local dir = utils.split_path(mp.get_property("path")) + local base = mp.get_property("filename/no-ext") + local seg = utils.join_path(dir, base .. ".seg.json") + local ivm = utils.join_path(dir, base .. ".ivm.json") + if not (utils.file_info(seg) and utils.file_info(ivm)) then + return + end + msg.info("Found JSON data files for interactive video playback:\n" .. + " seg: " .. seg .. "\n" .. + " ivm: " .. ivm) + + -- Read and parse JSON data files. + + local file + file = io.open(seg, "r") + if not file then + msg.error("Cannot read from " .. seg) + return + end + seg = utils.parse_json(file:read("*a")) + file:close() + + file = io.open(ivm, "r") + if not file then + msg.error("Cannot read from " .. ivm) + return + end + ivm = utils.parse_json(file:read("*a")) + file:close() + + -- Initialize data from JSON structures. + + segments = seg.segments + initial_segment = seg.initialSegment + local video_id = "" .. (seg.viewableId or "") + + if not segments then + msg.error( "segments not found in JSON data files") + return + elseif not initial_segment then + msg.error("initialSegment not found in JSON data files") + return + elseif not video_id then + msg.error( "viewableId not found in JSON data files") + return + end + + if ivm.videos and ivm.videos[video_id] and + ivm.videos[video_id].interactiveVideoMoments then + ivm = ivm.videos[video_id].interactiveVideoMoments.value + else + ivm = nil + end + if not ivm then + msg.error("interactiveVideoMoments not found in JSON data files") + return + end + + moments = ivm.momentsBySegment + preconditions = ivm.preconditions + segment_groups = ivm.segmentGroups + state.vars = shallow_copy(ivm.stateHistory) + + if not moments then + msg.error("momentsBySegment not found in JSON data files") + return + elseif not preconditions then + msg.error( "preconditions not found in JSON data files") + return + elseif not segment_groups then + msg.error( "segmentGroups not found in JSON data files") + return + elseif not state.vars then + msg.error( "stateHistory not found in JSON data files") + return + end + + -- Sanity checks. + + local fail = false + + -- Check segment history. + if not (ivm.segmentHistory and #ivm.segmentHistory == 1 and + ivm.segmentHistory[1] == initial_segment) then + msg.error("Invalid segmentHistory" .. + ": expected " .. utils.to_string({ initial_segment }) .. + ", got " .. utils.to_string(ivm.segmentHistory)) + fail = true + end + + -- Check segments. + for k, seg in pairs(segments) do + if (seg.endTimeMs or 1/0) <= seg.startTimeMs then + msg.error("Segment " .. k .. " ends before it starts") + fail = true + end + local has_scene = false + for i, mom in ipairs(moments[k] or {}) do + if string.sub(mom.type, 1, 6) == "scene:" and + not (mom.trackingInfo and + mom.trackingInfo.optionType == "fakeOption") then + has_scene = true + break + end + end + if count(seg.next) > 1 and not has_scene and not segment_groups[k] then + msg.error("Segment " .. k .. " requires a branching moment " .. + "or a segment group") + fail = true + end + end + + -- Check moments. + local n_moments = 0 + for k, ml in pairs(moments) do + n_moments = n_moments + #ml + for i, mi in ipairs(ml) do + local ki = k .. "/" .. i + + if mi.endMs <= mi.startMs then + msg.error("Moment " .. ki .. " ends before it starts") + fail = true + end + if mi.startMs < segments[k].startTimeMs then + msg.error("Moment " .. ki .. " starts before its segment does") + fail = true + end + if mi.endMs > (segments[k].endTimeMs or 1/0) then + msg.error("Moment " .. ki .. " ends after its segment does") + fail = true + end + + -- Scene moments. + if string.sub(mi.type, 1, 6) == "scene:" then + if not mi.choices then + msg.error("Moment " .. ki .. " has no branching choices") + fail = true + end + if not (mi.uiDisplayMS and mi.uiHideMS) then + msg.error("Moment " .. ki .. " has no display interval") + fail = true + else + if mi.uiHideMS <= mi.uiDisplayMS then + msg.error("Moment " .. ki .. "'s display interval is empty") + fail = true + end + if mi.uiDisplayMS < mi.startMs or mi.uiHideMS >= mi.endMs then + msg.warn ("Moment " .. ki .. "'s display interval overflows " .. + "out of bounds") + end + end + if mi.endMs < (segments[k].endTimeMs or 1/0) and + not (mi.trackingInfo and + mi.trackingInfo.optionType == "fakeOption") then + msg.warn ("Moment " .. ki .. " ends before its segment does") + end + + for j, mj in ipairs(ml) do + local kj = k .. "/" .. j + if i ~= j and mi.startMs <= mj.startMs and mj.startMs < mi.endMs then + msg.error("Moment " .. kj .. " starts while " .. ki .. + " is active") + fail = true + end + end + + -- Notification moments. + elseif mi.type == "notification:playbackImpression" then + if mi.choices then + msg.error("Moment " .. ki .. " has branching choices") + fail = true + end + if not mi.impressionData then + msg.warn ("Moment " .. ki .. " has no impression data") + end + + elseif mi.type == "notification:action" then + if mi.choices then + msg.error("Moment " .. ki .. " has branching choices") + fail = true + end + + else + msg.error ("Moment " .. ki .. " has invalid type: " .. mi.type) + fail = true + end + end + end + + if fail then + deactivate() + return + end + + -- Display stats. + + msg.verbose("Loaded JSON data files:\n" .. + " " .. count(segments) .. " segments\n" .. + " " .. n_moments .. " interactive moments\n" .. + " " .. count(preconditions) .. " preconditions\n" .. + " " .. count(segment_groups) .. " segment groups\n" .. + " " .. count(state.vars) .. " state variables") + + state.active = true + state.hist_idx = 0 + state.hist = {} + update_controls() +end + + +--[[ Jump to initial segment at beginning of playback ]]----------------------- + +function on_file_loaded(_) + if not state.active then return end + mp.register_event("tick", on_tick) + load_segment(initial_segment) + process_events(segments[initial_segment].startTimeMs, { ffwd = true }) +end + + +--[[ Process queued events on each frame ]]------------------------------------ + +function on_tick(_) + process_events() +end + + +--[[ Register script entry-point events ]]------------------------------------- + +mp.register_event("start-file", on_start_file) +mp.register_event("file-loaded", on_file_loaded) |
