demux_lavf: add support for programs

This adds proper support for MPEG-TS programs and HLS variants. Now
program selection is exposed as editions and tracks are grouped under
such edition. If there are external tracks added, they are available in
all editions.
This commit is contained in:
Kacper Michajłow committed 2026-04-17 18:30:43 +02:00
1 parent 8010ea49c9
commit 6c165fe46e
9 files changed
+286 -34

No files matched your search

@@ -0,0 +1,2 @@
``track-list`` is now filtered by the currently selected edition
add ``edition-list/N/metadata`` sub-property
+8 -2
View File
@@ -2411,6 +2411,7 @@ Property list
restart playback. The number of the first edition is 0.
For Matroska files, this is the edition. For DVD/Blu-ray, this is the title.
For MPEG-TS and HLS, this maps to the underlying program/variant.
This strictly returns the user-set option or property value, and the
``current-edition`` property returns the runtime selected edition (this
@@ -2447,6 +2448,9 @@ Property list
``edition-list/N/title``
Edition title as stored in the file. Not always available.
``edition-list/N/metadata``
Per-edition metadata key/value pairs.
When querying the property with the client API using ``MPV_FORMAT_NODE``,
or with Lua ``mp.get_property_native``, this will return a mpv_node with
the following contents:
@@ -2458,6 +2462,7 @@ Property list
"id" MPV_FORMAT_INT64
"title" MPV_FORMAT_STRING
"default" MPV_FORMAT_FLAG
"metadata" MPV_FORMAT_NODE_MAP
``metadata``
Metadata key/value pairs.
@@ -3392,7 +3397,8 @@ Property list
"playlist-path" MPV_FORMAT_STRING (optional)
``track-list``
List of audio/video/sub tracks, current entry marked.
List of audio/video/sub tracks, current entry marked. When the file has
editions, only tracks belonging to the currently selected edition are listed.
This has a number of sub-properties. Replace ``N`` with the 0-based track
index.
@@ -3466,7 +3472,7 @@ Property list
The bitrate of the HLS stream, if available.
``track-list/N/program-id``
The program ID of the HLS stream, if available.
The program ID of the stream, if available.
``track-list/N/codec``
The codec name used by this track, for example ``h264``. Unavailable
+1
View File
@@ -2991,6 +2991,7 @@ static void demux_copy(struct demuxer *dst, struct demuxer *src)
dst->editions = src->editions;
dst->num_editions = src->num_editions;
dst->edition = src->edition;
dst->edition_is_track_mapping = src->edition_is_track_mapping;
dst->attachments = src->attachments;
dst->num_attachments = src->num_attachments;
dst->matroska_data = src->matroska_data;
+1
View File
@@ -251,6 +251,7 @@ typedef struct demuxer {
struct demux_edition *editions;
int num_editions;
int edition;
bool edition_is_track_mapping;
struct demux_chapter *chapters;
int num_chapters;
+117
View File
@@ -53,6 +53,7 @@
#include "stheader.h"
#include "options/m_config.h"
#include "options/m_option.h"
#include "options/options.h"
#include "options/path.h"
#define INITIAL_PROBE_SIZE STREAM_BUFFER_SIZE
@@ -977,6 +978,121 @@ static int nested_io_close2(struct AVFormatContext *s, AVIOContext *pb)
return priv->default_io_close2(s, pb);
}
static void build_editions(demuxer_t *demuxer)
{
lavf_priv_t *priv = demuxer->priv;
AVFormatContext *avfc = priv->avfc;
if (avfc->nb_programs <= 1)
return;
for (unsigned i = 0; i < avfc->nb_programs; i++) {
AVProgram *prog = avfc->programs[i];
struct demux_edition ed = {
.demuxer_id = prog->id,
.metadata = talloc_zero(demuxer, struct mp_tags),
};
mp_tags_copy_from_av_dictionary(ed.metadata, prog->metadata);
// AVProgram is unlikely to have a "title" tag, but just in case,
// don't override it.
char *title = mp_tags_get_str(ed.metadata, "title");
if (title)
goto done;
char *service_name = mp_tags_get_str(ed.metadata, "service_name");
if (service_name) {
mp_tags_set_str(ed.metadata, "title", service_name);
goto done;
}
// If program/variant has a single video (or audio) stream, use the
// "comment" metadata as the name.
char *rendition_name = NULL;
int video_count = 0, audio_count = 0;
int video_idx = -1, audio_idx = -1;
for (unsigned j = 0; j < prog->nb_stream_indexes; j++) {
unsigned idx = prog->stream_index[j];
if (idx >= priv->num_streams || !priv->streams[idx]->sh)
continue;
if (priv->streams[idx]->sh->type == STREAM_VIDEO) {
video_count++;
video_idx = idx;
} else if (priv->streams[idx]->sh->type == STREAM_AUDIO) {
audio_count++;
audio_idx = idx;
}
}
int name_idx = video_count == 1 ? video_idx : audio_count == 1 ? audio_idx : -1;
if (name_idx >= 0)
rendition_name = mp_tags_get_str(priv->streams[name_idx]->sh->tags, "comment");
if (rendition_name) {
mp_tags_set_str(ed.metadata, "title", rendition_name);
goto done;
}
char *vb = mp_tags_get_str(ed.metadata, "variant_bitrate");
char *end;
double rate = vb ? strtoll(vb, &end, 10) : 0;
if (rate > 0 && *end == '\0') {
rate /= 1000.0;
if (rate < 1000) {
mp_tags_set_str(ed.metadata, "title", mp_tprintf(42, "Bitrate: %.f kbps", rate));
} else {
mp_tags_set_str(ed.metadata, "title", mp_tprintf(42, "Bitrate: %.3f Mbps", rate / 1000.0));
}
}
done:
MP_TARRAY_APPEND(demuxer, demuxer->editions, demuxer->num_editions, ed);
}
demuxer->edition_is_track_mapping = true;
struct MPOpts *mp_opts = mp_get_config_group(priv, demuxer->global, &mp_opt_root);
int hls_bitrate = mp_opts->hls_bitrate;
int edition_id = mp_opts->edition_id;
TA_FREEP(&mp_opts);
int selected = -1;
if (edition_id >= 0 && edition_id < demuxer->num_editions)
selected = edition_id;
// Select initial edition by best variant bitrate
if (selected < 0 && hls_bitrate >= 0) {
int best = -1;
int best_bitrate = 0;
bool best_ok = false;
for (int n = 0; n < demuxer->num_editions; n++) {
AVProgram *prog = avfc->programs[n];
for (unsigned j = 0; j < prog->nb_stream_indexes; j++) {
unsigned idx = prog->stream_index[j];
if (idx >= priv->num_streams || !priv->streams[idx]->sh)
continue;
struct sh_stream *sh = priv->streams[idx]->sh;
if (sh->type != STREAM_VIDEO || sh->hls_bitrate <= 0)
continue;
bool ok = sh->hls_bitrate <= hls_bitrate;
if (best < 0 || (ok && !best_ok) ||
(ok && best_ok && sh->hls_bitrate > best_bitrate) ||
(!ok && !best_ok && sh->hls_bitrate < best_bitrate))
{
best = n;
best_bitrate = sh->hls_bitrate;
best_ok = ok;
}
break;
}
}
if (best >= 0)
selected = best;
}
demuxer->edition = selected >= 0 ? selected : 0;
}
static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check)
{
AVFormatContext *avfc = NULL;
@@ -1139,6 +1255,7 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check)
}
add_new_streams(demuxer);
build_editions(demuxer);
mp_tags_move_from_av_dictionary(demuxer->metadata, &avfc->metadata);
+65 -10
View File
@@ -1179,15 +1179,26 @@ static int get_edition_entry(int item, int action, void *arg, void *ctx)
char *title = mp_tags_get_str(ed->metadata, "title");
struct mp_tags *tags = ed->metadata;
char **tag_list = talloc_zero_array(NULL, char *, tags->num_keys * 2 + 1);
for (int i = 0; i < tags->num_keys; i++) {
tag_list[2 * i] = talloc_strdup(tag_list, tags->keys[i]);
tag_list[2 * i + 1] = talloc_strdup(tag_list, tags->values[i]);
}
struct m_sub_property props[] = {
{"id", SUB_PROP_INT(item)},
{"title", SUB_PROP_STR(title),
.unavailable = !title},
{"default", SUB_PROP_BOOL(ed->default_edition)},
{"metadata", SUB_PROP_KEYVALUE_LIST(tag_list),
.unavailable = !tags->num_keys},
{0}
};
return m_property_read_sub(props, action, arg);
int ret = m_property_read_sub(props, action, arg);
talloc_free(tag_list);
return ret;
}
static int mp_property_list_editions(void *ctx, struct m_property *prop,
@@ -1964,7 +1975,7 @@ static struct track* track_next(struct MPContext *mpctx, enum stream_type type,
bool seen = track == NULL;
for (int n = 0; n < mpctx->num_tracks; n++) {
struct track *cur = mpctx->tracks[n];
if (cur->type == type) {
if (cur->type == type && track_in_current_edition(mpctx, cur)) {
if (cur == track) {
seen = true;
} else if (!cur->selected) {
@@ -2198,6 +2209,17 @@ static char *append_track_info(char *res, struct track *track)
return res;
}
struct filtered_track_ctx {
struct MPContext *mpctx;
int *map;
};
static int get_filtered_track_entry(int item, int action, void *arg, void *ctx)
{
struct filtered_track_ctx *ft = ctx;
return get_track_entry(ft->map[item], action, arg, ft->mpctx);
}
static int mp_property_list_tracks(void *ctx, struct m_property *prop,
int action, void *arg)
{
@@ -2211,6 +2233,8 @@ static int mp_property_list_tracks(void *ctx, struct m_property *prop,
struct track *track = mpctx->tracks[n];
if (track->type != type)
continue;
if (!track_in_current_edition(mpctx, track))
continue;
res = talloc_asprintf_append(res, "%s%s: ",
spacing == 2 ? "\n\n" : spacing == 1 ? "\n" : "",
track_type_name(track));
@@ -2256,9 +2280,10 @@ static int mp_property_list_tracks(void *ctx, struct m_property *prop,
type == STREAM_SUB ? "subtitle" : stream_type_name(type));
for (int n = 0; n < mpctx->num_tracks; n++) {
if (mpctx->tracks[n]->type == type) {
struct track *track = mpctx->tracks[n];
if (track->type == type && track_in_current_edition(mpctx, track)) {
res = talloc_strdup_append(res, "\n");
res = append_track_info(res, mpctx->tracks[n]);
res = append_track_info(res, track);
}
}
@@ -2270,8 +2295,16 @@ static int mp_property_list_tracks(void *ctx, struct m_property *prop,
}
}
return m_property_read_list(action, arg, mpctx->num_tracks,
get_track_entry, mpctx);
int *map = talloc_array(NULL, int, mpctx->num_tracks);
int count = 0;
for (int n = 0; n < mpctx->num_tracks; n++) {
if (track_in_current_edition(mpctx, mpctx->tracks[n]))
map[count++] = n;
}
struct filtered_track_ctx ft = { .mpctx = mpctx, .map = map };
int r = m_property_read_list(action, arg, count, get_filtered_track_entry, &ft);
talloc_free(map);
return r;
}
static int mp_property_current_tracks(void *ctx, struct m_property *prop,
@@ -2319,13 +2352,18 @@ static int mp_property_current_tracks(void *ctx, struct m_property *prop,
return M_PROPERTY_UNAVAILABLE;
int index = -1;
int filtered_index = 0;
for (int n = 0; n < mpctx->num_tracks; n++) {
if (!track_in_current_edition(mpctx, mpctx->tracks[n]))
continue;
if (mpctx->tracks[n] == t) {
index = n;
index = filtered_index;
break;
}
filtered_index++;
}
mp_assert(index >= 0);
if (index < 0)
return M_PROPERTY_UNAVAILABLE;
char *name = mp_tprintf(80, "track-list/%d%s%s", index, *rem ? "/" : "", rem);
return mp_property_do(name, ka->action, ka->arg, ctx);
@@ -8216,8 +8254,25 @@ void mp_option_run_callback(struct MPContext *mpctx, struct mp_option_callback *
struct demuxer *demuxer = mpctx->demuxer;
if (mpctx->playback_initialized && demuxer && demuxer->num_editions > 0) {
if (opts->edition_id != demuxer->edition) {
if (!mpctx->stop_play)
mpctx->stop_play = PT_CURRENT_ENTRY;
if (demuxer->edition_is_track_mapping) {
int new_ed = opts->edition_id;
if (new_ed < 0 || new_ed >= demuxer->num_editions)
new_ed = 0;
demuxer->edition = new_ed;
for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
for (int i = 0; i < num_ptracks[t]; i++) {
struct track *sel = select_default_track(mpctx, i, t);
if (sel != mpctx->current_track[i][t])
mp_switch_track_n(mpctx, i, t, sel, 0);
}
}
mp_notify_property(mpctx, "current-edition");
print_track_list(mpctx,
mp_tprintf(42, "Selected edition %d:", demuxer->edition));
} else {
if (!mpctx->stop_play)
mpctx->stop_play = PT_CURRENT_ENTRY;
}
mp_wakeup_core(mpctx);
}
}
+1
View File
@@ -551,6 +551,7 @@ struct playlist_entry *mp_next_file(struct MPContext *mpctx, int direction,
void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e);
void mp_play_files(struct MPContext *mpctx);
void update_demuxer_properties(struct MPContext *mpctx);
bool track_in_current_edition(struct MPContext *mpctx, struct track *track);
void print_track_list(struct MPContext *mpctx, const char *msg);
void reselect_demux_stream(struct MPContext *mpctx, struct track *track,
bool refresh_only);
+27 -9
View File
@@ -279,18 +279,31 @@ static void print_stream(struct MPContext *mpctx, struct track *t, bool indent)
MP_INFO(mpctx, "%s\n", b);
}
bool track_in_current_edition(struct MPContext *mpctx, struct track *track)
{
struct demuxer *demuxer = mpctx->demuxer;
if (!demuxer || !demuxer->edition_is_track_mapping || demuxer->num_editions <= 1)
return true;
if (track->is_external || track->program_id < 0)
return true;
return track->program_id == demuxer->editions[demuxer->edition].demuxer_id;
}
void print_track_list(struct MPContext *mpctx, const char *msg)
{
if (msg)
MP_INFO(mpctx, "%s\n", msg);
for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
for (int n = 0; n < mpctx->num_tracks; n++)
if (mpctx->tracks[n]->type == t)
// Indent tracks after messages like "Tracks switched" and
// "Playing:".
print_stream(mpctx, mpctx->tracks[n], msg ||
mpctx->playlist->num_entries > 1 ||
mpctx->playing->playlist_path);
for (int n = 0; n < mpctx->num_tracks; n++) {
struct track *track = mpctx->tracks[n];
if (track->type != t || !track_in_current_edition(mpctx, track))
continue;
// Indent tracks after messages like "Tracks switched" and
// "Playing:".
print_stream(mpctx, track, msg ||
mpctx->playlist->num_entries > 1 ||
mpctx->playing->playlist_path);
}
}
}
@@ -571,8 +584,13 @@ struct track *select_default_track(struct MPContext *mpctx, int order,
{
struct MPOpts *opts = mpctx->opts;
int tid = opts->stream_id[order][type];
int preferred_program = (type != STREAM_VIDEO && mpctx->current_track[0][STREAM_VIDEO]) ?
mpctx->current_track[0][STREAM_VIDEO]->program_id : -1;
const struct demuxer *demuxer = mpctx->demuxer;
int preferred_program = -1;
if (demuxer && demuxer->edition_is_track_mapping && demuxer->num_editions > 1) {
preferred_program = demuxer->editions[demuxer->edition].demuxer_id;
} else if (type != STREAM_VIDEO && mpctx->current_track[0][STREAM_VIDEO]) {
preferred_program = mpctx->current_track[0][STREAM_VIDEO]->program_id;
}
if (tid == -2)
return NULL;
char **langs = process_langs(opts->stream_lang[type]);
+64 -13
View File
@@ -302,27 +302,78 @@ static void test_track_selection(char *file, char *path)
check_string("track-list/5/selected", "yes");
} else if (strcmp(file, "multiprogram.ts") == 0) {
set_property_string("subs-match-os-language", "no");
set_property_string("vid", "2");
set_property_string("edition", "1");
reload_file(path);
check_string("current-edition", "1");
// no subs are selected by default
reload_file(path);
check_string("track-list/count", "2");
check_string("track-list/0/program-id", "2");
check_string("track-list/0/selected", "yes");
check_string("track-list/1/program-id", "2");
check_string("track-list/1/selected", "no");
check_string("track-list/3/selected", "no");
// the eng track cannot be selected due to having the wrong program ID
// because subs-fallback=default, the jpn track cannot be chosen either
// so we get no subs
set_property_string("slang", "eng");
reload_file(path);
check_string("track-list/1/selected", "no");
check_string("track-list/3/selected", "no");
// the jpn track is selected to match video program ID
// even though the user requested eng subtitles
set_property_string("subs-fallback", "yes");
// no eng subs in edition 1
reload_file(path);
check_string("track-list/count", "2");
check_string("track-list/0/program-id", "2");
check_string("track-list/0/selected", "yes");
check_string("track-list/1/program-id", "2");
check_string("track-list/1/selected", "no");
check_string("current-tracks/video/program-id", "2");
// eng subs in edition 0
set_property_string("edition", "0");
check_string("current-edition", "0");
check_string("track-list/count", "2");
check_string("track-list/0/program-id", "1");
check_string("track-list/0/selected", "yes");
check_string("track-list/1/program-id", "1");
check_string("track-list/1/lang", "eng");
check_string("track-list/1/selected", "yes");
check_string("current-tracks/sub/lang", "eng");
check_string("current-tracks/sub/program-id", "1");
check_string("current-tracks/video/program-id", "1");
// reload should give the same result
reload_file(path);
check_string("current-edition", "0");
check_string("track-list/count", "2");
check_string("track-list/0/program-id", "1");
check_string("track-list/0/selected", "yes");
check_string("track-list/1/program-id", "1");
check_string("track-list/1/lang", "eng");
check_string("track-list/1/selected", "yes");
check_string("current-tracks/sub/lang", "eng");
check_string("current-tracks/sub/program-id", "1");
check_string("current-tracks/video/program-id", "1");
set_property_string("subs-fallback", "yes");
set_property_string("edition", "1");
// the jpn track is selected even though the user requested eng subtitles
check_string("current-edition", "1");
check_string("track-list/count", "2");
check_string("track-list/0/selected", "yes");
check_string("current-tracks/sub/lang", "jpn");
check_string("track-list/3/selected", "yes");
check_string("track-list/1/selected", "yes");
check_string("current-edition", "1");
check_string("track-list/count", "2");
check_string("track-list/0/program-id", "2");
check_string("track-list/0/selected", "yes");
check_string("track-list/1/program-id", "2");
check_string("track-list/1/lang", "jpn");
check_string("track-list/1/selected", "yes");
check_string("current-tracks/sub/lang", "jpn");
check_string("current-tracks/sub/program-id", "2");
check_string("current-tracks/video/program-id", "2");
}
}