diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15b2141..dd44bca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,11 +32,11 @@ jobs: cmake -DCMAKE_BUILD_TYPE=RELEASE \ -DCMAKE_SHARED_LINKER_FLAGS="-static" \ -G Ninja .. - cmake --build . + cmake --build . --target package - uses: actions/upload-artifact@v3 with: name: menu - path: build/menu.dll + path: build/menu.zip publish: needs: build runs-on: ubuntu-22.04 @@ -44,8 +44,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/download-artifact@v3 - - name: Zip files - run: zip -r menu.zip menu - uses: rickstaa/action-create-tag@v1 if: ${{ github.ref == 'refs/heads/main' }} with: @@ -55,7 +53,7 @@ jobs: with: commit: ${{ github.sha }} tag: ${{ github.ref == 'refs/heads/main' && 'dev' || github.ref_name }} - artifacts: "menu.zip" + artifacts: "menu/menu.zip" allowUpdates: true prerelease: ${{ github.ref == 'refs/heads/main' }} name: ${{ github.ref == 'refs/heads/main' && 'dev' || github.ref_name }} diff --git a/CMakeLists.txt b/CMakeLists.txt index b625f3e..cc25a53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,11 +10,11 @@ set(CMAKE_SHARED_LIBRARY_PREFIX "") add_library(menu SHARED mpv/misc/bstr.c mpv/misc/dispatch.c + mpv/misc/node.c mpv/ta/ta.c mpv/ta/ta_talloc.c mpv/ta/ta_utils.c - src/types.c src/menu.c src/plugin.c ) @@ -23,3 +23,10 @@ set_property(TARGET menu PROPERTY POSITION_INDEPENDENT_CODE ON) target_include_directories(menu PRIVATE mpv ${MPV_INCLUDE_DIRS}) target_link_libraries(menu PRIVATE shlwapi) target_compile_definitions(menu PRIVATE MPV_CPLUGIN_DYNAMIC_SYM) + +install(TARGETS menu RUNTIME DESTINATION .) + +set(CPACK_GENERATOR ZIP) +set(CPACK_PACKAGE_FILE_NAME menu) +set(CPACK_INSTALLED_DIRECTORIES ${CMAKE_SOURCE_DIR}/lua .) +include(CPack) diff --git a/README.md b/README.md index 0039170..f64d931 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ See also [mpv-debug-plugin](https://github.com/tsl0922/mpv-debug-plugin). [mpv](https://mpv.io) >= `0.37.0` is required, and the `cplugins` feature should be enabled. -Download the plugin from Releases, and place `menu.dll` in your mpv `scripts` folder. +Download the plugin from Releases, place `menu.dll` and `dyn_menu.lua` in your mpv `scripts` folder. ## Configuration @@ -24,13 +24,14 @@ The menu syntax is similar to [mpv.net](https://github.com/mpvnet-player/mpv.net - define separator with `-` - split title with `>` to define submenus - use `#@keyword` to display selection menu for: - - `#@tracks/video`: video tracks - - `#@tracks/audio`: audio tracks - - `#@tracks/sub`: subtitles - - `#@tracks/sub-secondary`: secondary subtitle - - `#@chapters`: chapters - - `#@editions`: editions - - `#@audio-devices`: audio devices + - `#@tracks`: track list (video/audio/subtitle) + - `#@tracks/video`: video track list + - `#@tracks/audio`: audio track list + - `#@tracks/sub`: subtitle list + - `#@tracks/sub-secondary`: subtitle list (secondary) + - `#@chapters`: chapter list + - `#@editions`: edition list + - `#@audio-devices`: audio device list - use `_` if no keybinding - use `ignore` if no command @@ -38,12 +39,7 @@ The menu syntax is similar to [mpv.net](https://github.com/mpvnet-player/mpv.net Ctrl+a show-text foobar #menu: Foo > Bar _ ignore #menu: - -_ ignore #menu: Tracks > Video #@tracks/video -_ ignore #menu: Tracks > Audio #@tracks/audio -_ ignore #menu: - -_ ignore #menu: Subtitle #@tracks/sub -_ ignore #menu: Second Subtitle #@tracks/sub-secondary -_ ignore #menu: - +_ ignore #menu: Tracks #@tracks _ ignore #menu: Chapters #@chapters _ ignore #menu: Editions #@editions _ ignore #menu: - @@ -56,10 +52,31 @@ Add a keybinding to trigger the menu (required): MBTN_RIGHT script-message-to menu show ``` +> **NOTE:** If the menu doesn't always show on mouse click, Rename other scripts that used the `menu` name. +> +> If both `menu.dll` and `menu.lua` exists in scripts folder, one of it may be named with `menu2` by mpv, +> `script-message-to menu show` will break when it happens on `menu.dll`. + ### ~~/script-opts/menu.conf - `uosc=yes`: Enalbe [uosc](https://github.com/tomasklaen/uosc#syntax) menu syntax support. +## Updating menu from another plugin + +The menu data is stored in `user-data/menu/items` property with the following structure: + +``` +MPV_FORMAT_NODE_ARRAY + MPV_FORMAT_NODE_MAP (menu item) + "type" MPV_FORMAT_STRING (supported type: separator, submenu) + "title" MPV_FORMAT_STRING (required if type is not separator) + "cmd" MPV_FORMAT_STRING (optional) + "state" MPV_FORMAT_NODE_ARRAY[MPV_FORMAT_STRING] (supported state: checked, disabled) + "submenu" MPV_FORMAT_NODE_ARRAY[menu item] (required if type is submenu) +``` + +Updating this property will trigger an update of the menu UI. + ## Credits This project contains code copied from [mpv](https://github.com/mpv-player/mpv). diff --git a/lua/dyn_menu.lua b/lua/dyn_menu.lua new file mode 100644 index 0000000..7489788 --- /dev/null +++ b/lua/dyn_menu.lua @@ -0,0 +1,257 @@ +-- Copyright (c) 2023 tsl0922. All rights reserved. +-- SPDX-License-Identifier: GPL-2.0-only +-- +-- #@keyword support for dynamic menu +-- +-- supported keywords: +-- #@tracks: video/audio/sub tracks +-- #@tracks/video: video track list +-- #@tracks/audio: audio track list +-- #@tracks/sub: subtitle list +-- #@tracks/sub-secondary: subtitle list (secondary) +-- #@chapters: chapter list +-- #@editions: edition list +-- #@audio-devices: audio device list + +local menu_prop = 'user-data/menu/items' +local menu_items = mp.get_property_native(menu_prop, {}) + +function build_track_title(track, prefix, filename) + local title = track.title or '' + local codec = track.codec or '' + local type = track.type + + if title ~= '' and filename ~= '' then title = title:gsub(filename .. '%.?', '') end + if title ~= '' and title:lower() == codec:lower() then title = '' end + if title == '' then + local name = type:sub(1, 1):upper() .. type:sub(2, #type) + title = string.format('%s %d', name, track.id) + end + + local hints = {} + local function h(value) hints[#hints + 1] = value end + if codec ~= '' then h(codec) end + if track['demux-h'] then + h(track['demux-w'] and (track['demux-w'] .. 'x' .. track['demux-h'] or track['demux-h'] .. 'p')) + end + if track['demux-fps'] then h(string.format('%.5gfps', track['demux-fps'])) end + if track['audio-channels'] then h(track['audio-channels'] .. 'ch') end + if track['demux-samplerate'] then h(string.format('%.3gkHz', track['demux-samplerate'] / 1000)) end + if track.forced then h('forced') end + if track.default then h('default') end + if track.external then h('external') end + if #hints > 0 then title = string.format('%s [%s]', title, table.concat(hints, ', ')) end + + if track.lang then title = string.format('%s\t%s', title, track.lang:upper()) end + if prefix then title = string.format('%s: %s', type:sub(1, 1):upper(), title) end + return title +end + +function build_track_items(list, type, prop, prefix) + local items = {} + + local filename = mp.get_property('filename/no-ext', ''):gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%0") + local pos = mp.get_property_number(prop, -1) + for _, track in ipairs(list) do + if track.type == type then + local state = {} + if track.selected then + table.insert(state, 'checked') + if track.id ~= pos then table.insert(state, 'disabled') end + end + + items[#items + 1] = { + title = build_track_title(track, prefix, filename), + cmd = string.format('set %s %d', prop, track.id), + state = state, + } + end + end + + if #items > 0 then + local title = pos > 0 and 'Off' or 'Auto' + local value = pos > 0 and 'no' or 'auto' + if prefix then title = string.format('%s: %s', type:sub(1, 1):upper(), title) end + + items[#items + 1] = { + title = title, + cmd = string.format('set %s %s', prop, value), + } + end + + return items +end + +function update_tracks_menu(submenu) + mp.observe_property('track-list', 'native', function(_, track_list) + for i = #submenu, 1, -1 do table.remove(submenu, i) end + if not track_list then return end + + local items_v = build_track_items(track_list, 'video', 'vid', true) + local items_a = build_track_items(track_list, 'audio', 'aid', true) + local items_s = build_track_items(track_list, 'sub', 'sid', true) + + for _, item in ipairs(items_v) do table.insert(submenu, item) end + if #submenu > 0 and #items_a > 0 then table.insert(submenu, { type = 'separator' }) end + for _, item in ipairs(items_a) do table.insert(submenu, item) end + if #submenu > 0 and #items_s > 0 then table.insert(submenu, { type = 'separator' }) end + for _, item in ipairs(items_s) do table.insert(submenu, item) end + + mp.set_property_native(menu_prop, menu_items) + end) +end + +function update_track_menu(submenu, type, prop) + mp.observe_property('track-list', 'native', function(_, track_list) + for i = #submenu, 1, -1 do table.remove(submenu, i) end + if not track_list then return end + + local items = build_track_items(track_list, type, prop, false) + for _, item in ipairs(items) do table.insert(submenu, item) end + + mp.set_property_native(menu_prop, menu_items) + end) +end + +function update_chapters_menu(submenu) + mp.observe_property('chapter-list', 'native', function(_, chapter_list) + for i = #submenu, 1, -1 do table.remove(submenu, i) end + if not chapter_list then return end + + local pos = mp.get_property_number('chapter', -1) + for id, chapter in ipairs(chapter_list) do + local title = chapter.title or '' + if title == '' then title = 'Chapter ' .. id end + local time = string.format('%02d:%02d:%02d', chapter.time / 3600, chapter.time / 60 % 60, chapter.time % 60) + + submenu[#submenu + 1] = { + title = string.format('%s\t[%s]', title, time), + cmd = string.format('seek %f absolute', chapter.time), + state = id == pos + 1 and { 'checked' } or {}, + } + end + + mp.set_property_native(menu_prop, menu_items) + end) + + mp.observe_property('chapter', 'number', function(_, pos) + if not pos then return end + for id, item in ipairs(submenu) do + item.state = id == pos + 1 and { 'checked' } or {} + end + mp.set_property_native(menu_prop, menu_items) + end) +end + +function update_editions_menu(submenu) + mp.observe_property('edition-list', 'native', function(_, editions) + for i = #submenu, 1, -1 do table.remove(submenu, i) end + if not editions then return end + + local current = mp.get_property_number('current-edition', -1) + for id, edition in ipairs(editions) do + local title = edition.title or '' + if title == '' then title = 'Edition ' .. id end + if edition.default then title = title .. ' [default]' end + submenu[#submenu + 1] = { + title = title, + cmd = string.format('set edition %d', id - 1), + state = id == current + 1 and { 'checked' } or {}, + } + end + + mp.set_property_native(menu_prop, menu_items) + end) + + mp.observe_property('current-edition', 'number', function(_, pos) + if not pos then return end + for id, item in ipairs(submenu) do + item.state = id == pos + 1 and { 'checked' } or {} + end + mp.set_property_native(menu_prop, menu_items) + end) +end + +function update_audio_devices_menu(submenu) + mp.observe_property('audio-device-list', 'native', function(_, devices) + for i = #submenu, 1, -1 do table.remove(submenu, i) end + if not devices then return end + + local current = mp.get_property('audio-device', '') + for _, device in ipairs(devices) do + submenu[#submenu + 1] = { + title = device.description or device.name, + cmd = string.format('set audio-device %s', device.name), + state = device.name == current and { 'checked' } or {}, + } + end + + mp.set_property_native(menu_prop, menu_items) + end) + + mp.observe_property('audio-device', 'string', function(_, name) + if not name then return end + for _, item in ipairs(submenu) do + item.state = item.cmd:match('%s*set audio%-device%s+(%S+)%s*$') == name and { 'checked' } or {} + end + mp.set_property_native(menu_prop, menu_items) + end) +end + +function update_dyn_menu(submenu, keyword) + if keyword == 'tracks' then + update_tracks_menu(submenu) + elseif keyword == 'tracks/video' then + update_track_menu(submenu, "video", "vid") + elseif keyword == 'tracks/audio' then + update_track_menu(submenu, "audio", "aid") + elseif keyword == 'tracks/sub' then + update_track_menu(submenu, "sub", "sid") + elseif keyword == 'tracks/sub-secondary' then + update_track_menu(submenu, "sub", "secondary-sid") + elseif keyword == 'chapters' then + update_chapters_menu(submenu) + elseif keyword == 'editions' then + update_editions_menu(submenu) + elseif keyword == 'audio-devices' then + update_audio_devices_menu(submenu) + end +end + +function check_keyword(items) + if not items then return end + for _, item in ipairs(items) do + if item.type == 'submenu' then + check_keyword(item.submenu) + else + if item.type ~= 'separator' and item.cmd then + local keyword = item.cmd:match('%s*#@([%S]+).-%s*$') or '' + if keyword ~= '' then + local submenu = {} + + item.type = 'submenu' + item.submenu = submenu + item.cmd = nil + + mp.set_property_native(menu_prop, menu_items) + update_dyn_menu(submenu, keyword) + end + end + end + end +end + +function update_menu(name, items) + if items and #items > 0 then + mp.unobserve_property(update_menu) + + menu_items = items + check_keyword(items) + end +end + +if #menu_items > 0 then + check_keyword(menu_items) +else + mp.observe_property(menu_prop, 'native', update_menu) +end diff --git a/mpv/misc/node.c b/mpv/misc/node.c new file mode 100644 index 0000000..b27acdb --- /dev/null +++ b/mpv/misc/node.c @@ -0,0 +1,202 @@ +#include +#include "node.h" + +// Init a node with the given format. If parent is not NULL, it is set as +// parent allocation according to m_option_type_node rules (which means +// the mpv_node_list allocs are used for chaining the TA allocations). +// format == MPV_FORMAT_NONE will simply initialize it with all-0. +void node_init(struct mpv_node *dst, int format, struct mpv_node *parent) { + // Other formats need to be initialized manually. + assert(format == MPV_FORMAT_NODE_MAP || format == MPV_FORMAT_NODE_ARRAY || + format == MPV_FORMAT_FLAG || format == MPV_FORMAT_INT64 || + format == MPV_FORMAT_DOUBLE || format == MPV_FORMAT_BYTE_ARRAY || + format == MPV_FORMAT_NONE); + + void *ta_parent = NULL; + if (parent) { + assert(parent->format == MPV_FORMAT_NODE_MAP || + parent->format == MPV_FORMAT_NODE_ARRAY); + ta_parent = parent->u.list; + } + + *dst = (struct mpv_node){.format = format}; + if (format == MPV_FORMAT_NODE_MAP || format == MPV_FORMAT_NODE_ARRAY) + dst->u.list = talloc_zero(ta_parent, struct mpv_node_list); + if (format == MPV_FORMAT_BYTE_ARRAY) + dst->u.ba = talloc_zero(ta_parent, struct mpv_byte_array); +} + +// Add an entry to a MPV_FORMAT_NODE_ARRAY. +// m_option_type_node memory management rules apply. +struct mpv_node *node_array_add(struct mpv_node *dst, int format) { + struct mpv_node_list *list = dst->u.list; + assert(dst->format == MPV_FORMAT_NODE_ARRAY && dst->u.list); + MP_TARRAY_GROW(list, list->values, list->num); + node_init(&list->values[list->num], format, dst); + return &list->values[list->num++]; +} + +// Add an entry to a MPV_FORMAT_NODE_MAP. Keep in mind that this does +// not check for already existing entries under the same key. +// m_option_type_node memory management rules apply. +struct mpv_node *node_map_add(struct mpv_node *dst, const char *key, + int format) { + assert(key); + return node_map_badd(dst, bstr0(key), format); +} + +struct mpv_node *node_map_badd(struct mpv_node *dst, struct bstr key, + int format) { + assert(key.start); + + struct mpv_node_list *list = dst->u.list; + assert(dst->format == MPV_FORMAT_NODE_MAP && dst->u.list); + MP_TARRAY_GROW(list, list->values, list->num); + MP_TARRAY_GROW(list, list->keys, list->num); + list->keys[list->num] = bstrdup0(list, key); + node_init(&list->values[list->num], format, dst); + return &list->values[list->num++]; +} + +// Add a string entry to a MPV_FORMAT_NODE_MAP. Keep in mind that this does +// not check for already existing entries under the same key. +// m_option_type_node memory management rules apply. +void node_map_add_string(struct mpv_node *dst, const char *key, + const char *val) { + assert(val); + + struct mpv_node *entry = node_map_add(dst, key, MPV_FORMAT_NONE); + entry->format = MPV_FORMAT_STRING; + entry->u.string = talloc_strdup(dst->u.list, val); +} + +void node_map_add_int64(struct mpv_node *dst, const char *key, int64_t v) { + node_map_add(dst, key, MPV_FORMAT_INT64)->u.int64 = v; +} + +void node_map_add_double(struct mpv_node *dst, const char *key, double v) { + node_map_add(dst, key, MPV_FORMAT_DOUBLE)->u.double_ = v; +} + +void node_map_add_flag(struct mpv_node *dst, const char *key, bool v) { + node_map_add(dst, key, MPV_FORMAT_FLAG)->u.flag = v; +} + +mpv_node *node_map_get(mpv_node *src, const char *key) { + return node_map_bget(src, bstr0(key)); +} + +mpv_node *node_map_bget(mpv_node *src, struct bstr key) { + if (src->format != MPV_FORMAT_NODE_MAP) return NULL; + + for (int i = 0; i < src->u.list->num; i++) { + if (bstr_equals0(key, src->u.list->keys[i])) + return &src->u.list->values[i]; + } + + return NULL; +} + +// Note: for MPV_FORMAT_NODE_MAP, this (incorrectly) takes the order into +// account, instead of treating it as set. +bool equal_mpv_value(const void *a, const void *b, mpv_format format) { + switch (format) { + case MPV_FORMAT_NONE: + return true; + case MPV_FORMAT_STRING: + case MPV_FORMAT_OSD_STRING: + return strcmp(*(char **)a, *(char **)b) == 0; + case MPV_FORMAT_FLAG: + return *(int *)a == *(int *)b; + case MPV_FORMAT_INT64: + return *(int64_t *)a == *(int64_t *)b; + case MPV_FORMAT_DOUBLE: + return *(double *)a == *(double *)b; + case MPV_FORMAT_NODE: + return equal_mpv_node(a, b); + case MPV_FORMAT_BYTE_ARRAY: { + const struct mpv_byte_array *a_r = a, *b_r = b; + if (a_r->size != b_r->size) return false; + return memcmp(a_r->data, b_r->data, a_r->size) == 0; + } + case MPV_FORMAT_NODE_ARRAY: + case MPV_FORMAT_NODE_MAP: { + mpv_node_list *l_a = *(mpv_node_list **)a, + *l_b = *(mpv_node_list **)b; + if (l_a->num != l_b->num) return false; + for (int n = 0; n < l_a->num; n++) { + if (format == MPV_FORMAT_NODE_MAP) { + if (strcmp(l_a->keys[n], l_b->keys[n]) != 0) return false; + } + if (!equal_mpv_node(&l_a->values[n], &l_b->values[n])) + return false; + } + return true; + } + } + MP_ASSERT_UNREACHABLE(); // supposed to be able to handle all defined types +} + +// Remarks see equal_mpv_value(). +bool equal_mpv_node(const struct mpv_node *a, const struct mpv_node *b) { + if (a->format != b->format) return false; + return equal_mpv_value(&a->u, &b->u, a->format); +} + +static void dup_node(void *ta_parent, struct mpv_node *node) { + switch (node->format) { + case MPV_FORMAT_STRING: + node->u.string = talloc_strdup(ta_parent, node->u.string); + break; + case MPV_FORMAT_NODE_ARRAY: + case MPV_FORMAT_NODE_MAP: { + struct mpv_node_list *oldlist = node->u.list; + struct mpv_node_list *new = + talloc_zero(ta_parent, struct mpv_node_list); + node->u.list = new; + if (oldlist->num > 0) { + *new = *oldlist; + new->values = talloc_array(new, struct mpv_node, new->num); + for (int n = 0; n < new->num; n++) { + new->values[n] = oldlist->values[n]; + dup_node(new, &new->values[n]); + } + if (node->format == MPV_FORMAT_NODE_MAP) { + new->keys = talloc_array(new, char *, new->num); + for (int n = 0; n < new->num; n++) + new->keys[n] = talloc_strdup(new, oldlist->keys[n]); + } + } + break; + } + case MPV_FORMAT_BYTE_ARRAY: { + struct mpv_byte_array *old = node->u.ba; + struct mpv_byte_array *new = + talloc_zero(ta_parent, struct mpv_byte_array); + node->u.ba = new; + if (old->size > 0) { + *new = *old; + new->data = talloc_memdup(new, old->data, old->size); + } + break; + } + case MPV_FORMAT_NONE: + case MPV_FORMAT_FLAG: + case MPV_FORMAT_INT64: + case MPV_FORMAT_DOUBLE: + break; + default: + // unknown entry - mark as invalid + node->format = (mpv_format)-1; + } +} + +void copy_mpv_node(struct mpv_node *dst, struct mpv_node *src) { + if (!dst || !src) return; + + talloc_free_children(dst); + *dst = (struct mpv_node){0}; + + *dst = *src; + dup_node(dst, dst); +} \ No newline at end of file diff --git a/mpv/misc/node.h b/mpv/misc/node.h new file mode 100644 index 0000000..dce6b33 --- /dev/null +++ b/mpv/misc/node.h @@ -0,0 +1,21 @@ +#ifndef MP_MISC_NODE_H_ +#define MP_MISC_NODE_H_ + +#include "mpv/client.h" +#include "misc/bstr.h" + +void node_init(struct mpv_node *dst, int format, struct mpv_node *parent); +struct mpv_node *node_array_add(struct mpv_node *dst, int format); +struct mpv_node *node_map_add(struct mpv_node *dst, const char *key, int format); +struct mpv_node *node_map_badd(struct mpv_node *dst, struct bstr key, int format); +void node_map_add_string(struct mpv_node *dst, const char *key, const char *val); +void node_map_add_int64(struct mpv_node *dst, const char *key, int64_t v); +void node_map_add_double(struct mpv_node *dst, const char *key, double v); +void node_map_add_flag(struct mpv_node *dst, const char *key, bool v); +mpv_node *node_map_get(mpv_node *src, const char *key); +mpv_node *node_map_bget(mpv_node *src, struct bstr key); +bool equal_mpv_value(const void *a, const void *b, mpv_format format); +bool equal_mpv_node(const struct mpv_node *a, const struct mpv_node *b); +void copy_mpv_node(struct mpv_node *dst, struct mpv_node *src); + +#endif diff --git a/src/menu.c b/src/menu.c index 81b1255..b46b9af 100644 --- a/src/menu.c +++ b/src/menu.c @@ -2,99 +2,19 @@ // SPDX-License-Identifier: GPL-2.0-only #include "misc/bstr.h" +#include "misc/node.h" #include "menu.h" #define MENU_PREFIX "#menu:" #define MENU_PREFIX_UOSC "#!" -#define MENU_PREFIX_DYN "#@" - -typedef struct dyn_item { - HMENU hmenu; // submenu handle - UINT id; // menu command id - void *talloc_ctx; // talloc context - void (*update)(mp_state *state, struct dyn_item *item); -} dyn_entry; - -typedef struct { - dyn_entry *entries; - int num_entries; -} dyn_list; - -typedef struct { - char *keyword; // keyword in menu title - void (*update)(mp_state *state, dyn_entry *item); -} dyn_provider; - -// forward declarations -static void update_video_track_menu(mp_state *state, dyn_entry *item); -static void update_audio_track_menu(mp_state *state, dyn_entry *item); -static void update_sub_track_menu(mp_state *state, dyn_entry *item); -static void update_sub_track_menu2(mp_state *state, dyn_entry *item); -static void update_chapter_menu(mp_state *state, dyn_entry *item); -static void update_edition_menu(mp_state *state, dyn_entry *item); -static void update_audio_device_menu(mp_state *state, dyn_entry *item); - -// dynamic menu providers -static const dyn_provider dyn_providers[] = { - {"tracks/video", update_video_track_menu}, - {"tracks/audio", update_audio_track_menu}, - {"tracks/sub", update_sub_track_menu}, - {"tracks/sub-secondary", update_sub_track_menu2}, - {"chapters", update_chapter_menu}, - {"editions", update_edition_menu}, - {"audio-devices", update_audio_device_menu}, -}; - -// dynamic menu list -static dyn_list *dyn_menus = NULL; - -static bool add_dyn_menu(void *talloc_ctx, HMENU hmenu, int id, bstr keyword) { - for (int i = 0; i < ARRAYSIZE(dyn_providers); i++) { - dyn_provider provider = dyn_providers[i]; - if (!bstr_equals0(keyword, provider.keyword)) continue; - - MP_TARRAY_APPEND(talloc_ctx, dyn_menus->entries, dyn_menus->num_entries, - (dyn_entry){ - .hmenu = hmenu, - .id = id, - .talloc_ctx = talloc_new(talloc_ctx), - .update = provider.update, - }); - return true; - } - return false; -} - -static HMENU find_submenu(HMENU hmenu, wchar_t *name, UINT *id) { - MENUITEMINFOW mii; - int count = GetMenuItemCount(hmenu); - - for (int i = 0; i < count; i++) { - memset(&mii, 0, sizeof(mii)); - mii.cbSize = sizeof(mii); - mii.fMask = MIIM_STRING; - if (!GetMenuItemInfoW(hmenu, i, TRUE, &mii) || mii.cch == 0) continue; - - mii.cch++; - wchar_t buf[mii.cch]; - mii.dwTypeData = buf; - mii.fMask |= MIIM_ID | MIIM_SUBMENU; - if (!GetMenuItemInfoW(hmenu, i, TRUE, &mii) || !mii.hSubMenu) continue; - if (wcscmp(mii.dwTypeData, name) == 0) { - if (id) *id = mii.wID; - return mii.hSubMenu; - } - } - return NULL; -} // escape & to && for menu title -static wchar_t *escape_title(void *talloc_ctx, bstr title) { +static wchar_t *escape_title(void *talloc_ctx, char *title) { void *tmp = talloc_new(NULL); bstr left, rest; bstr escaped = bstr0(NULL); - left = bstr_split(title, "&", &rest); + left = bstr_split(bstr0(title), "&", &rest); while (rest.len > 0) { bstr_xappend(tmp, &escaped, left); bstr_xappend(tmp, &escaped, bstr0("&&")); @@ -107,21 +27,7 @@ static wchar_t *escape_title(void *talloc_ctx, bstr title) { return ret; } -// format title as name\tkey -static wchar_t *format_title(void *talloc_ctx, bstr name, bstr key) { - void *tmp = talloc_new(NULL); - bstr title = bstrdup(tmp, name); - - if (key.len > 0 && !bstr_equals0(key, "_")) { - bstr_xappend(tmp, &title, bstr0("\t")); - bstr_xappend(tmp, &title, key); - } - - wchar_t *ret = escape_title(talloc_ctx, title); - talloc_free(tmp); - return ret; -} - +// append menu item to HMENU static int append_menu(HMENU hmenu, UINT fMask, UINT fType, UINT fState, wchar_t *title, HMENU submenu, void *data) { static UINT id = WM_USER + 100; @@ -143,168 +49,127 @@ static int append_menu(HMENU hmenu, UINT fMask, UINT fType, UINT fState, return InsertMenuItemW(hmenu, -1, TRUE, &mii) ? mii.wID : -1; } -static int append_seprator(HMENU hmenu) { - return append_menu(hmenu, MIIM_FTYPE, MFT_SEPARATOR, 0, NULL, NULL, NULL); -} - -static HMENU append_submenu(HMENU hmenu, wchar_t *title, int *id) { - HMENU menu = find_submenu(hmenu, title, id); - if (menu != NULL) return menu; - - menu = CreatePopupMenu(); - int wid = - append_menu(hmenu, MIIM_STRING | MIIM_SUBMENU, 0, 0, title, menu, NULL); - if (id) *id = wid; - return menu; -} - -static void update_track_menu(mp_state *state, dyn_entry *item, - const char *type, const char *prop, int64_t pos) { - mp_track_list *list = state->track_list; - if (list == NULL || list->num_entries == 0) return; - - int count = GetMenuItemCount(item->hmenu); +// build fState for menu item creation +static UINT build_state(mpv_node *node) { + UINT fState = 0; + for (int i = 0; i < node->u.list->num; i++) { + mpv_node *item = &node->u.list->values[i]; + if (item->format != MPV_FORMAT_STRING) continue; - for (int i = 0; i < list->num_entries; i++) { - mp_track_item *entry = &list->entries[i]; - if (strcmp(entry->type, type) != 0) continue; - - UINT fState = entry->selected ? MFS_CHECKED : MFS_UNCHECKED; - if (strcmp(type, "sub") == 0 && entry->selected && pos != entry->id) + if (strcmp(item->u.string, "checked") == 0) { + fState |= MFS_CHECKED; + } else if (strcmp(item->u.string, "disabled") == 0) { fState |= MFS_DISABLED; - append_menu( - item->hmenu, MIIM_STRING | MIIM_DATA | MIIM_STATE, 0, fState, - format_title(item->talloc_ctx, bstr0(entry->title), - bstr0(entry->lang)), - NULL, - talloc_asprintf(item->talloc_ctx, "set %s %d", prop, entry->id)); - } - - if (GetMenuItemCount(item->hmenu) > count) { - append_menu(item->hmenu, MIIM_STRING | MIIM_DATA | MIIM_STATE, 0, - pos < 0 ? MFS_CHECKED : MFS_UNCHECKED, - escape_title(item->talloc_ctx, bstr0("Off")), NULL, - talloc_asprintf(item->talloc_ctx, "set %s no", prop)); - } -} - -static void update_video_track_menu(mp_state *state, dyn_entry *item) { - update_track_menu(state, item, "video", "vid", state->vid); -} - -static void update_audio_track_menu(mp_state *state, dyn_entry *item) { - update_track_menu(state, item, "audio", "aid", state->aid); -} - -static void update_sub_track_menu(mp_state *state, dyn_entry *item) { - update_track_menu(state, item, "sub", "sid", state->sid); -} - -static void update_sub_track_menu2(mp_state *state, dyn_entry *item) { - update_track_menu(state, item, "sub", "secondary-sid", state->sid2); -} - -static void update_chapter_menu(mp_state *state, dyn_entry *item) { - mp_chapter_list *list = state->chapter_list; - if (list == NULL || list->num_entries == 0) return; - - void *tmp = talloc_new(NULL); - - for (int i = 0; i < list->num_entries; i++) { - mp_chapter_item *entry = &list->entries[i]; - const char *time = - talloc_asprintf(tmp, "[%02d:%02d:%02d]", (int)entry->time / 3600, - (int)entry->time / 60 % 60, (int)entry->time % 60); - append_menu( - item->hmenu, MIIM_STRING | MIIM_DATA, 0, 0, - format_title(item->talloc_ctx, bstr0(entry->title), bstr0(time)), - NULL, - talloc_asprintf(item->talloc_ctx, "seek %f absolute", entry->time)); - } - if (state->chapter >= 0) { - CheckMenuRadioItem(item->hmenu, 0, list->num_entries, state->chapter, - MF_BYPOSITION); - } - - talloc_free(tmp); -} - -static void update_edition_menu(mp_state *state, dyn_entry *item) { - mp_edition_list *list = state->edition_list; - if (list == NULL || list->num_entries == 0) return; - - int pos = -1; - for (int i = 0; i < list->num_entries; i++) { - mp_edition_item *entry = &list->entries[i]; - if (entry->id == state->edition) pos = i; - append_menu( - item->hmenu, MIIM_STRING | MIIM_DATA, 0, 0, - escape_title(item->talloc_ctx, bstr0(entry->title)), NULL, - talloc_asprintf(item->talloc_ctx, "set edition %d", entry->id)); - } - if (pos >= 0) { - CheckMenuRadioItem(item->hmenu, 0, list->num_entries, pos, - MF_BYPOSITION); + } } + return fState; } -static void update_audio_device_menu(mp_state *state, dyn_entry *item) { - mp_audio_device_list *list = state->audio_device_list; - if (list == NULL || list->num_entries == 0) return; - - void *tmp = talloc_new(NULL); +// build HMENU from mpv node +// +// node structure: +// +// MPV_FORMAT_NODE_ARRAY +// MPV_FORMAT_NODE_MAP (menu item) +// "type" MPV_FORMAT_STRING +// "title" MPV_FORMAT_STRING +// "cmd" MPV_FORMAT_STRING +// "state" MPV_FORMAT_NODE_ARRAY[MPV_FORMAT_STRING] +// "submenu" MPV_FORMAT_NODE_ARRAY[menu item] +static void build_menu(void *talloc_ctx, HMENU hmenu, mpv_node *node) { + if (node->format != MPV_FORMAT_NODE_ARRAY || node->u.list->num == 0) return; + + for (int i = 0; i < node->u.list->num; i++) { + mpv_node *item = &node->u.list->values[i]; + if (item->format != MPV_FORMAT_NODE_MAP) continue; + + mpv_node_list *list = item->u.list; + + char *type = ""; + char *title = NULL; + char *cmd = NULL; + UINT fState = 0; + HMENU submenu = NULL; + + for (int j = 0; j < list->num; j++) { + char *key = list->keys[j]; + mpv_node *value = &list->values[j]; + + switch (value->format) { + case MPV_FORMAT_STRING: + if (strcmp(key, "title") == 0) { + title = value->u.string; + } else if (strcmp(key, "cmd") == 0) { + cmd = value->u.string; + } else if (strcmp(key, "type") == 0) { + type = value->u.string; + } + break; + case MPV_FORMAT_NODE_ARRAY: + if (strcmp(key, "state") == 0) { + fState = build_state(value); + } else if (strcmp(key, "submenu") == 0) { + submenu = CreatePopupMenu(); + build_menu(talloc_ctx, submenu, value); + } + break; + } + } - char *name = state->audio_device; - int pos = -1; - for (int i = 0; i < list->num_entries; i++) { - mp_audio_device *entry = &list->entries[i]; - if (strcmp(entry->name, name) == 0) pos = i; - char *title = entry->desc; - if (title == NULL || strlen(title) == 0) - title = talloc_strdup(tmp, entry->name); - append_menu(item->hmenu, MIIM_STRING | MIIM_DATA, 0, 0, - escape_title(item->talloc_ctx, bstr0(title)), NULL, - talloc_asprintf(item->talloc_ctx, "set audio-device %s", - entry->name)); - } - if (pos >= 0) { - CheckMenuRadioItem(item->hmenu, 0, list->num_entries, pos, - MF_BYPOSITION); + if (strcmp(type, "separator") == 0) { + append_menu(hmenu, MIIM_FTYPE, MFT_SEPARATOR, 0, NULL, NULL, NULL); + } else { + if (title == NULL || strlen(title) == 0) continue; + + UINT fMask = MIIM_STRING | MIIM_STATE; + bool grayed = false; + if (strcmp(type, "submenu") == 0) { + if (submenu == NULL) submenu = CreatePopupMenu(); + fMask |= MIIM_SUBMENU; + grayed = GetMenuItemCount(submenu) == 0; + } else { + fMask |= MIIM_DATA; + grayed = cmd == NULL || cmd[0] == '\0' || cmd[0] == '#' || + strcmp(cmd, "ignore") == 0; + } + int id = append_menu(hmenu, fMask, 0, fState, + escape_title(talloc_ctx, title), submenu, + talloc_strdup(talloc_ctx, cmd)); + if (grayed) EnableMenuItem(hmenu, id, MF_BYCOMMAND | MF_GRAYED); + } } - - talloc_free(tmp); } -static void dyn_menu_init(void *talloc_ctx) { - dyn_menus = talloc_zero(talloc_ctx, dyn_list); +static bool is_seprarator(bstr text, bool uosc) { + return bstr_equals0(text, "-") || (uosc && bstr_startswith0(text, "---")); } -static void dyn_menu_update(plugin_ctx *ctx) { - if (dyn_menus == NULL) return; - - for (int i = 0; i < dyn_menus->num_entries; i++) { - dyn_entry *item = &dyn_menus->entries[i]; - - // clear menu - while (GetMenuItemCount(item->hmenu) > 0) - RemoveMenu(item->hmenu, 0, MF_BYPOSITION); - talloc_free_children(item->talloc_ctx); +// return submenu node if exists, otherwise create a new one +static mpv_node *add_submenu(mpv_node *list, char *title) { + for (int i = 0; i < list->u.list->num; i++) { + mpv_node *item = &list->u.list->values[i]; - item->update(ctx->state, item); + mpv_node *type_node = node_map_get(item, "type"); + if (!type_node || strcmp(type_node->u.string, "submenu") != 0) continue; - // update state - int count = GetMenuItemCount(item->hmenu); - UINT enable = count > 0 ? MF_ENABLED : MF_GRAYED; - EnableMenuItem(ctx->hmenu, item->id, MF_BYCOMMAND | enable); + mpv_node *title_node = node_map_get(item, "title"); + if (title_node && strcmp(title_node->u.string, title) == 0) { + mpv_node *submenu = node_map_get(item, "submenu"); + if (!submenu) + submenu = node_map_add(item, "submenu", MPV_FORMAT_NODE_ARRAY); + return submenu; + } } -} -static bool is_seprarator(bstr text, bool uosc) { - return bstr_equals0(text, "-") || (uosc && bstr_startswith0(text, "---")); + mpv_node *node = node_array_add(list, MPV_FORMAT_NODE_MAP); + node_map_add_string(node, "title", title); + node_map_add_string(node, "type", "submenu"); + return node_map_add(node, "submenu", MPV_FORMAT_NODE_ARRAY); } -static void parse_menu(void *talloc_ctx, HMENU hmenu, bstr key, bstr cmd, - bstr text, bool uosc) { +// parse menu line and add to menu node +static void parse_menu(mpv_node *list, bstr key, bstr cmd, bstr text, + bool uosc) { bstr name, rest, comment; name = bstr_split(text, ">", &rest); @@ -312,32 +177,23 @@ static void parse_menu(void *talloc_ctx, HMENU hmenu, bstr key, bstr cmd, name = bstr_strip(name); if (!name.len) return; + void *tmp = talloc_new(NULL); + if (!rest.len) { if (is_seprarator(name, uosc)) { - append_seprator(hmenu); + mpv_node *node = node_array_add(list, MPV_FORMAT_NODE_MAP); + node_map_add_string(node, "type", "separator"); } else { - int id = 0; - if (bstr_eatstart0(&comment, MENU_PREFIX_DYN)) { - HMENU submenu = - append_submenu(hmenu, escape_title(talloc_ctx, name), &id); - if (id > 0 && comment.len > 0) { - bstr keyword = bstr_split(comment, "#", NULL); - keyword = bstr_rstrip(keyword); - add_dyn_menu(talloc_ctx, submenu, id, keyword); - } - } else { - id = append_menu(hmenu, MIIM_STRING | MIIM_DATA, 0, 0, - format_title(talloc_ctx, name, key), NULL, - bstrdup0(talloc_ctx, cmd)); - if (id > 0 && (!cmd.len || bstr_startswith0(cmd, "#"))) - EnableMenuItem(hmenu, id, MF_BYCOMMAND | MF_GRAYED); - } + mpv_node *node = node_array_add(list, MPV_FORMAT_NODE_MAP); + node_map_add_string(node, "title", bstrdup0(tmp, name)); + node_map_add_string(node, "cmd", bstrdup0(tmp, cmd)); } } else { - HMENU submenu = - append_submenu(hmenu, escape_title(talloc_ctx, name), NULL); - if (!comment.len) parse_menu(talloc_ctx, submenu, key, cmd, rest, uosc); + mpv_node *sub = add_submenu(list, bstrdup0(tmp, name)); + if (!comment.len) parse_menu(sub, key, cmd, rest, uosc); } + + talloc_free(tmp); } static bool split_menu(bstr line, bstr *left, bstr *right, bool uosc) { @@ -351,15 +207,14 @@ static bool split_menu(bstr line, bstr *left, bstr *right, bool uosc) { return right->len > 0; } +// build menu node from input.conf and create HMENU HMENU load_menu(plugin_ctx *ctx) { - dyn_menu_init(ctx); - void *tmp = talloc_new(NULL); char *path = mp_get_prop_string(tmp, "input-conf"); if (path == NULL || strlen(path) == 0) path = "~~/input.conf"; - HMENU hmenu = CreatePopupMenu(); bstr data = bstr0(mp_read_file(tmp, path)); + node_init(ctx->node, MPV_FORMAT_NODE_ARRAY, NULL); while (data.len > 0) { bstr line = bstr_strip_linebreaks(bstr_getline(data, &data)); @@ -376,27 +231,43 @@ HMENU load_menu(plugin_ctx *ctx) { cmd = bstr_strip(cmd); } if (split_menu(cmd, &left, &right, ctx->conf->uosc)) - parse_menu(ctx, hmenu, key, cmd, right, ctx->conf->uosc); + parse_menu(ctx->node, key, cmd, right, ctx->conf->uosc); } + HMENU hmenu = CreatePopupMenu(); + build_menu(ctx->hmenu_ctx, hmenu, ctx->node); + mpv_set_property(ctx->mpv, MENU_DATA_PROP, MPV_FORMAT_NODE, ctx->node); + talloc_free(tmp); return hmenu; } +// update HMENU if menu node changed +void update_menu(plugin_ctx *ctx, mpv_node *node) { + if (equal_mpv_node(ctx->node, node)) return; + + while (GetMenuItemCount(ctx->hmenu) > 0) + RemoveMenu(ctx->hmenu, 0, MF_BYPOSITION); + talloc_free_children(ctx->hmenu_ctx); + + build_menu(ctx->hmenu_ctx, ctx->hmenu, node); + copy_mpv_node(ctx->node, node); +} + +// show menu at position if it is in window void show_menu(plugin_ctx *ctx, POINT *pt) { RECT rc; GetClientRect(ctx->hwnd, &rc); ScreenToClient(ctx->hwnd, pt); if (!PtInRect(&rc, *pt)) return; - dyn_menu_update(ctx); - ClientToScreen(ctx->hwnd, pt); TrackPopupMenuEx(ctx->hmenu, TPM_LEFTALIGN | TPM_LEFTBUTTON, pt->x, pt->y, ctx->hwnd, NULL); } +// run mpv command stored in menu item data void handle_menu(plugin_ctx *ctx, int id) { MENUITEMINFOW mii = {0}; mii.cbSize = sizeof(mii); diff --git a/src/menu.h b/src/menu.h index 3a68277..d566665 100644 --- a/src/menu.h +++ b/src/menu.h @@ -8,7 +8,10 @@ #include #include "plugin.h" +#define MENU_DATA_PROP "user-data/menu/items" + HMENU load_menu(plugin_ctx *ctx); +void update_menu(plugin_ctx *ctx, mpv_node *node); void show_menu(plugin_ctx *ctx, POINT *pt); void handle_menu(plugin_ctx *ctx, int id); diff --git a/src/plugin.c b/src/plugin.c index f572c77..a33ecac 100644 --- a/src/plugin.c +++ b/src/plugin.c @@ -55,7 +55,6 @@ static void plugin_read_conf(plugin_config *conf, const char *name) { } static void plugin_register(int64_t wid) { - ctx->hmenu = load_menu(ctx); ctx->hwnd = (HWND)wid; ctx->wnd_proc = (WNDPROC)SetWindowLongPtrW(ctx->hwnd, GWLP_WNDPROC, (LONG_PTR)WndProc); @@ -63,12 +62,18 @@ static void plugin_register(int64_t wid) { static void handle_property_change(mpv_event *event) { mpv_event_property *prop = event->data; - if (prop->format == MPV_FORMAT_INT64 && - strcmp(prop->name, "window-id") == 0) { - int64_t wid = *(int64_t *)prop->data; - if (wid > 0) plugin_register(wid); - } else { - mp_state_update(ctx->state, event); + switch (prop->format) { + case MPV_FORMAT_INT64: + if (strcmp(prop->name, "window-id") == 0) { + int64_t wid = *(int64_t *)prop->data; + if (wid > 0) plugin_register(wid); + } + break; + case MPV_FORMAT_NODE: + if (strcmp(prop->name, MENU_DATA_PROP) == 0) { + update_menu(ctx, prop->data); + } + break; } } @@ -83,7 +88,8 @@ static void handle_client_message(mpv_event *event) { static plugin_ctx *create_plugin_ctx() { plugin_ctx *ctx = talloc_zero(NULL, plugin_ctx); ctx->conf = talloc_zero(ctx, plugin_config); - ctx->state = talloc_zero(ctx, mp_state); + ctx->node = talloc_zero(ctx, mpv_node); + ctx->hmenu_ctx = talloc_new(ctx); return ctx; } @@ -97,11 +103,12 @@ static void destroy_plugin_ctx() { MPV_EXPORT int mpv_open_cplugin(mpv_handle *handle) { ctx = create_plugin_ctx(NULL); ctx->mpv = handle; + ctx->hmenu = load_menu(ctx); ctx->dispatch = mp_dispatch_create(ctx); plugin_read_conf(ctx->conf, mpv_client_name(handle)); mpv_observe_property(handle, 0, "window-id", MPV_FORMAT_INT64); - mp_state_init(ctx->state, handle); + mpv_observe_property(handle, 0, MENU_DATA_PROP, MPV_FORMAT_NODE); while (handle) { mpv_event *event = mpv_wait_event(handle, -1); diff --git a/src/plugin.h b/src/plugin.h index 105b0f2..931aa94 100644 --- a/src/plugin.h +++ b/src/plugin.h @@ -7,7 +7,6 @@ #include #include #include "misc/dispatch.h" -#include "types.h" typedef struct { bool uosc; // use uosc menu syntax @@ -18,10 +17,11 @@ typedef struct { struct mp_dispatch_queue *dispatch; // dispatch queue mpv_handle *mpv; // mpv client handle - mp_state *state; // cached mpv properties + mpv_node *node; // menu property node HWND hwnd; // window handle - HMENU hmenu; // menu handle + HMENU hmenu; // native menu handle + void *hmenu_ctx; // menu talloc context WNDPROC wnd_proc; // previous window procedure } plugin_ctx; diff --git a/src/types.c b/src/types.c deleted file mode 100644 index 3972f7d..0000000 --- a/src/types.c +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) 2023 tsl0922. All rights reserved. -// SPDX-License-Identifier: GPL-2.0-only - -#include -#include "mpv_talloc.h" -#include "misc/ctype.h" -#include "types.h" - -static void update_track_list(mp_state *state, mpv_node *node) { - if (state->track_list != NULL) talloc_free(state->track_list); - state->track_list = talloc_zero(state, mp_track_list); - mp_track_list *list = state->track_list; - - for (int i = 0; i < node->u.list->num; i++) { - mpv_node track = node->u.list->values[i]; - - mp_track_item item = {0}; - - for (int j = 0; j < track.u.list->num; j++) { - char *key = track.u.list->keys[j]; - mpv_node value = track.u.list->values[j]; - if (strcmp(key, "id") == 0) { - item.id = value.u.int64; - } else if (strcmp(key, "title") == 0) { - item.title = talloc_strdup(list, value.u.string); - } else if (strcmp(key, "lang") == 0) { - item.lang = talloc_strdup(list, value.u.string); - } else if (strcmp(key, "type") == 0) { - item.type = talloc_strdup(list, value.u.string); - } else if (strcmp(key, "selected") == 0) { - item.selected = value.u.flag; - } - } - - // set default title if not set - if (item.title == NULL) - item.title = talloc_asprintf(list, "%s %d", item.type, item.id); - - // convert lang to uppercase - if (item.lang != NULL) { - for (int x = 0; item.lang[x]; x++) - item.lang[x] = mp_toupper(item.lang[x]); - } - - MP_TARRAY_APPEND(list, list->entries, list->num_entries, item); - } -} - -static void update_chapter_list(mp_state *state, mpv_node *node) { - if (state->chapter_list != NULL) talloc_free(state->chapter_list); - state->chapter_list = talloc_zero(state, mp_chapter_list); - mp_chapter_list *list = state->chapter_list; - - for (int i = 0; i < node->u.list->num; i++) { - mpv_node chapter = node->u.list->values[i]; - - mp_chapter_item item = {0}; - - for (int j = 0; j < chapter.u.list->num; j++) { - char *key = chapter.u.list->keys[j]; - mpv_node value = chapter.u.list->values[j]; - if (strcmp(key, "title") == 0) { - item.title = talloc_strdup(list, value.u.string); - } else if (strcmp(key, "time") == 0) { - item.time = value.u.double_; - } - } - - // set default title if not set - if (item.title == NULL) - item.title = talloc_asprintf(list, "chapter %d", i + 1); - - MP_TARRAY_APPEND(list, list->entries, list->num_entries, item); - } -} - -static void update_edition_list(mp_state *state, mpv_node *node) { - if (state->edition_list != NULL) talloc_free(state->edition_list); - state->edition_list = talloc_zero(state, mp_edition_list); - mp_edition_list *list = state->edition_list; - - for (int i = 0; i < node->u.list->num; i++) { - mpv_node edition = node->u.list->values[i]; - - mp_edition_item item = {0}; - - for (int j = 0; j < edition.u.list->num; j++) { - char *key = edition.u.list->keys[j]; - mpv_node value = edition.u.list->values[j]; - if (strcmp(key, "title") == 0) { - item.title = talloc_strdup(list, value.u.string); - } else if (strcmp(key, "id") == 0) { - item.id = value.u.int64; - } - } - - // set default title if not set - if (item.title == NULL) - item.title = talloc_asprintf(list, "edition %d", item.id); - - MP_TARRAY_APPEND(list, list->entries, list->num_entries, item); - } -} - -static void update_audio_device_list(mp_state *state, mpv_node *node) { - if (state->audio_device_list != NULL) talloc_free(state->audio_device_list); - state->audio_device_list = talloc_zero(state, mp_audio_device_list); - mp_audio_device_list *list = state->audio_device_list; - - for (int i = 0; i < node->u.list->num; i++) { - mpv_node device = node->u.list->values[i]; - - mp_audio_device item = {0}; - - for (int j = 0; j < device.u.list->num; j++) { - char *key = device.u.list->keys[j]; - mpv_node value = device.u.list->values[j]; - if (strcmp(key, "name") == 0) { - item.name = talloc_strdup(list, value.u.string); - } else if (strcmp(key, "description") == 0) { - item.desc = talloc_strdup(list, value.u.string); - } - } - - MP_TARRAY_APPEND(list, list->entries, list->num_entries, item); - } -} - -void mp_state_init(mp_state *state, mpv_handle *mpv) { - mpv_observe_property(mpv, 0, "vid", MPV_FORMAT_INT64); - mpv_observe_property(mpv, 0, "aid", MPV_FORMAT_INT64); - mpv_observe_property(mpv, 0, "sid", MPV_FORMAT_INT64); - mpv_observe_property(mpv, 0, "secondary-sid", MPV_FORMAT_INT64); - mpv_observe_property(mpv, 0, "chapter", MPV_FORMAT_INT64); - mpv_observe_property(mpv, 0, "current-edition", MPV_FORMAT_INT64); - mpv_observe_property(mpv, 0, "audio-device", MPV_FORMAT_STRING); - mpv_observe_property(mpv, 0, "track-list", MPV_FORMAT_NODE); - mpv_observe_property(mpv, 0, "chapter-list", MPV_FORMAT_NODE); - mpv_observe_property(mpv, 0, "edition-list", MPV_FORMAT_NODE); - mpv_observe_property(mpv, 0, "audio-device-list", MPV_FORMAT_NODE); -} - -void mp_state_update(mp_state *state, mpv_event *event) { - mpv_event_property *prop = event->data; - switch (prop->format) { - case MPV_FORMAT_NONE: - if (strcmp(prop->name, "vid") == 0) { - state->vid = -1; - } else if (strcmp(prop->name, "aid") == 0) { - state->aid = -1; - } else if (strcmp(prop->name, "sid") == 0) { - state->sid = -1; - } else if (strcmp(prop->name, "secondary-sid") == 0) { - state->sid2 = -1; - } - break; - case MPV_FORMAT_INT64: - if (strcmp(prop->name, "vid") == 0) { - state->vid = *(int64_t *)prop->data; - } else if (strcmp(prop->name, "aid") == 0) { - state->aid = *(int64_t *)prop->data; - } else if (strcmp(prop->name, "sid") == 0) { - state->sid = *(int64_t *)prop->data; - } else if (strcmp(prop->name, "secondary-sid") == 0) { - state->sid2 = *(int64_t *)prop->data; - } else if (strcmp(prop->name, "chapter") == 0) { - state->chapter = *(int64_t *)prop->data; - } else if (strcmp(prop->name, "current-edition") == 0) { - state->edition = *(int64_t *)prop->data; - } - break; - case MPV_FORMAT_STRING: - if (strcmp(prop->name, "audio-device") == 0) { - if (state->audio_device != NULL) - talloc_free(state->audio_device); - char *val = *(char **)prop->data; - state->audio_device = talloc_strdup(state, val); - } - break; - case MPV_FORMAT_NODE: - if (strcmp(prop->name, "track-list") == 0) { - update_track_list(state, prop->data); - } else if (strcmp(prop->name, "chapter-list") == 0) { - update_chapter_list(state, prop->data); - } else if (strcmp(prop->name, "edition-list") == 0) { - update_edition_list(state, prop->data); - } else if (strcmp(prop->name, "audio-device-list") == 0) { - update_audio_device_list(state, prop->data); - } - break; - } -} \ No newline at end of file diff --git a/src/types.h b/src/types.h deleted file mode 100644 index f56e76f..0000000 --- a/src/types.h +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2023 tsl0922. All rights reserved. -// SPDX-License-Identifier: GPL-2.0-only - -#ifndef MPV_PLUGIN_TYPES_H -#define MPV_PLUGIN_TYPES_H - -#include - -typedef struct { - int64_t id; // the ID as it's used for -sid/--aid/--vid - char *type; // string describing the media type - char *title; // track title as it is stored in the file - char *lang; // track language as identified by the file - bool selected; // if the track is currently decoded -} mp_track_item; - -typedef struct { - mp_track_item *entries; - int num_entries; -} mp_track_list; - -typedef struct { - char *title; // chapter title as stored in the file - double time; // chapter start time in seconds as float -} mp_chapter_item; - -typedef struct { - mp_chapter_item *entries; - int num_entries; -} mp_chapter_list; - -typedef struct { - char *title; // edition title as stored in the file - int64_t id; // edition ID as integer -} mp_edition_item; - -typedef struct { - mp_edition_item *entries; - int num_entries; -} mp_edition_list; - -typedef struct { - char *name; // device name - char *desc; // device description -} mp_audio_device; - -typedef struct { - mp_audio_device *entries; - int num_entries; -} mp_audio_device_list; - -typedef struct { - int64_t vid; // video ID - int64_t aid; // audio ID - int64_t sid; // subtitle ID - int64_t sid2; // secondary subtitle ID - int64_t chapter; // chapter ID - int64_t edition; // edition ID - - char *audio_device; // audio device name - - mp_track_list *track_list; // track list - mp_chapter_list *chapter_list; // chapter list - mp_edition_list *edition_list; // edition list - mp_audio_device_list *audio_device_list; // audio device list -} mp_state; - -void mp_state_init(mp_state *state, mpv_handle *mpv); -void mp_state_update(mp_state *state, mpv_event *event); - -#endif \ No newline at end of file