mirror of
https://github.com/mpv-player/mpv
synced 2025-02-20 06:46:55 +00:00
player: add experimental stream recording feature
This is basically a WIP, but it can't remain in a branch forever. A warning is print when using it as it's still a bit "shaky".
This commit is contained in:
parent
061b752217
commit
96a45a16af
@ -32,6 +32,7 @@ Interface changes
|
||||
- "vo-drop-frame-count" to "frame-drop-count"
|
||||
The old names still work, but are deprecated.
|
||||
- remove the --stream-capture option and property. No replacement.
|
||||
(--stream-record might serve as alternative)
|
||||
- add --sub-justify
|
||||
- add --sub-ass-justify
|
||||
- internally there's a different way to enable the demuxer cache now
|
||||
|
@ -4863,6 +4863,29 @@ Miscellaneous
|
||||
This does not affect playlist expansion, redirection, or other loading of
|
||||
referenced files like with ordered chapters.
|
||||
|
||||
``--record-stream=<file>``
|
||||
Record the current stream to the given target file. The target file will
|
||||
always be overwritten without asking.
|
||||
|
||||
This remuxes the source stream without reencoding, which makes this a
|
||||
highly fragile and experimental feature. It's entirely possible that this
|
||||
writes files which are broken, not standards compliant, not playable with
|
||||
all players (including mpv), or incomplete.
|
||||
|
||||
The target file format is determined by the file extension of the target
|
||||
filename. It is recommended to use the same target container as the source
|
||||
container if possible, and preferring Matroska as fallback.
|
||||
|
||||
Seeking during stream recording, or enabling/disabling stream recording
|
||||
during playback, can cut off data, or produce "holes" in the output file.
|
||||
These are technical restrictions. In particular, video data or subtitles
|
||||
which were read ahead can produce such holes, which might cause playback
|
||||
problems with various players (including mpv).
|
||||
|
||||
The behavior of this option might changed in the future, such as changing
|
||||
it to a template (similar to ``--screenshot-template``), being renamed,
|
||||
removed, or anything else, until it is declared semi-stable.
|
||||
|
||||
``--lavfi-complex=<string>``
|
||||
Set a "complex" libavfilter filter, which means a single filter graph can
|
||||
take input from multiple source audio and video tracks. The graph can result
|
||||
|
@ -27,6 +27,7 @@
|
||||
|
||||
#include "common/codecs.h"
|
||||
#include "common/msg.h"
|
||||
#include "common/recorder.h"
|
||||
#include "misc/bstr.h"
|
||||
|
||||
#include "stream/stream.h"
|
||||
@ -218,6 +219,9 @@ void audio_work(struct dec_audio *da)
|
||||
}
|
||||
|
||||
if (da->ad_driver->send_packet(da, da->packet)) {
|
||||
if (da->recorder_sink)
|
||||
mp_recorder_feed_packet(da->recorder_sink, da->packet);
|
||||
|
||||
talloc_free(da->packet);
|
||||
da->packet = NULL;
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ struct dec_audio {
|
||||
|
||||
bool try_spdif;
|
||||
|
||||
struct mp_recorder_sink *recorder_sink;
|
||||
|
||||
// For free use by the ad_driver
|
||||
void *priv;
|
||||
|
||||
|
384
common/recorder.c
Normal file
384
common/recorder.c
Normal file
@ -0,0 +1,384 @@
|
||||
/*
|
||||
* This file is part of mpv.
|
||||
*
|
||||
* mpv is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* mpv is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#include <libavformat/avformat.h>
|
||||
|
||||
#include "common/av_common.h"
|
||||
#include "common/common.h"
|
||||
#include "common/global.h"
|
||||
#include "common/msg.h"
|
||||
#include "demux/packet.h"
|
||||
#include "demux/stheader.h"
|
||||
|
||||
#include "recorder.h"
|
||||
|
||||
#define PTS_ADD(a, b) ((a) == MP_NOPTS_VALUE ? (a) : ((a) + (b)))
|
||||
|
||||
// Maximum number of packets we buffer at most to attempt to resync streams.
|
||||
// Essentially, this should be higher than the highest supported keyframe
|
||||
// interval.
|
||||
#define QUEUE_MAX_PACKETS 256
|
||||
// Number of packets we should buffer at least to determine timestamps (due to
|
||||
// codec delay and frame reordering, and potentially lack of DTS).
|
||||
// Keyframe flags can trigger this earlier.
|
||||
#define QUEUE_MIN_PACKETS 16
|
||||
|
||||
struct mp_recorder {
|
||||
struct mpv_global *global;
|
||||
struct mp_log *log;
|
||||
|
||||
struct mp_recorder_sink **streams;
|
||||
int num_streams;
|
||||
|
||||
bool opened; // mux context is valid
|
||||
bool muxing; // we're currently recording (instead of preparing)
|
||||
bool muxing_from_start; // no discontinuity at start
|
||||
bool dts_warning;
|
||||
|
||||
// The start timestamp of the currently recorded segment (the timestamp of
|
||||
// the first packet of the incoming packet stream).
|
||||
double base_ts;
|
||||
// The output packet timestamp corresponding to base_ts. It's the timestamp
|
||||
// of the first packet of the current segment written to the output.
|
||||
double rebase_ts;
|
||||
|
||||
AVFormatContext *mux;
|
||||
};
|
||||
|
||||
struct mp_recorder_sink {
|
||||
struct mp_recorder *owner;
|
||||
struct sh_stream *sh;
|
||||
AVStream *av_stream;
|
||||
double max_out_pts;
|
||||
bool discont;
|
||||
bool proper_eof;
|
||||
struct demux_packet **packets;
|
||||
int num_packets;
|
||||
};
|
||||
|
||||
static int add_stream(struct mp_recorder *priv, struct sh_stream *sh)
|
||||
{
|
||||
enum AVMediaType av_type = mp_to_av_stream_type(sh->type);
|
||||
if (av_type == AVMEDIA_TYPE_UNKNOWN)
|
||||
return -1;
|
||||
|
||||
struct mp_recorder_sink *rst = talloc(priv, struct mp_recorder_sink);
|
||||
*rst = (struct mp_recorder_sink) {
|
||||
.owner = priv,
|
||||
.sh = sh,
|
||||
.av_stream = avformat_new_stream(priv->mux, NULL),
|
||||
.max_out_pts = MP_NOPTS_VALUE,
|
||||
};
|
||||
|
||||
if (!rst->av_stream)
|
||||
return -1;
|
||||
|
||||
AVCodecParameters *avp = mp_codec_params_to_av(sh->codec);
|
||||
if (!avp)
|
||||
return -1;
|
||||
|
||||
#if LIBAVCODEC_VERSION_MICRO >= 100
|
||||
// We don't know the delay, so make something up. If the format requires
|
||||
// DTS, the result will probably be broken. FFmpeg provides nothing better
|
||||
// yet (unless you demux with libavformat, which contains tons of hacks
|
||||
// that try to determine a PTS).
|
||||
if (!sh->codec->lav_codecpar)
|
||||
avp->video_delay = 16;
|
||||
#endif
|
||||
|
||||
if (avp->codec_id == AV_CODEC_ID_NONE)
|
||||
return -1;
|
||||
|
||||
if (avcodec_parameters_copy(rst->av_stream->codecpar, avp) < 0)
|
||||
return -1;
|
||||
|
||||
rst->av_stream->time_base = mp_get_codec_timebase(sh->codec);
|
||||
|
||||
MP_TARRAY_APPEND(priv, priv->streams, priv->num_streams, rst);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct mp_recorder *mp_recorder_create(struct mpv_global *global,
|
||||
const char *target_file,
|
||||
struct sh_stream **streams,
|
||||
int num_streams)
|
||||
{
|
||||
struct mp_recorder *priv = talloc_zero(NULL, struct mp_recorder);
|
||||
|
||||
priv->global = global;
|
||||
priv->log = mp_log_new(priv, global->log, "recorder");
|
||||
|
||||
if (!num_streams) {
|
||||
MP_ERR(priv, "No streams.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
priv->mux = avformat_alloc_context();
|
||||
if (!priv->mux)
|
||||
goto error;
|
||||
|
||||
priv->mux->oformat = av_guess_format(NULL, target_file, NULL);
|
||||
if (!priv->mux->oformat) {
|
||||
MP_ERR(priv, "Output format not found.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (avio_open2(&priv->mux->pb, target_file, AVIO_FLAG_WRITE, NULL, NULL) < 0) {
|
||||
MP_ERR(priv, "Failed opening output file.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
for (int n = 0; n < num_streams; n++) {
|
||||
if (add_stream(priv, streams[n]) < 0) {
|
||||
MP_ERR(priv, "Can't mux one of the input streams.\n");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
// Not sure how to write this in a "standard" way. It appears only mkv
|
||||
// and mp4 support this directly.
|
||||
char version[200];
|
||||
snprintf(version, sizeof(version), "%s experimental stream recording "
|
||||
"feature (can generate broken files - please report bugs)",
|
||||
mpv_version);
|
||||
av_dict_set(&priv->mux->metadata, "encoding_tool", version, 0);
|
||||
|
||||
if (avformat_write_header(priv->mux, NULL) < 0) {
|
||||
MP_ERR(priv, "Write header failed.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
priv->opened = true;
|
||||
priv->muxing_from_start = true;
|
||||
|
||||
priv->base_ts = MP_NOPTS_VALUE;
|
||||
priv->rebase_ts = 0;
|
||||
|
||||
MP_WARN(priv, "This is an experimental feature. Output files might be "
|
||||
"broken or not play correctly with various players "
|
||||
"(including mpv itself).\n");
|
||||
|
||||
return priv;
|
||||
|
||||
error:
|
||||
mp_recorder_destroy(priv);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void flush_packets(struct mp_recorder *priv)
|
||||
{
|
||||
for (int n = 0; n < priv->num_streams; n++) {
|
||||
struct mp_recorder_sink *rst = priv->streams[n];
|
||||
for (int i = 0; i < rst->num_packets; i++)
|
||||
talloc_free(rst->packets[i]);
|
||||
rst->num_packets = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void mux_packet(struct mp_recorder_sink *rst,
|
||||
struct demux_packet *pkt)
|
||||
{
|
||||
struct mp_recorder *priv = rst->owner;
|
||||
struct demux_packet mpkt = *pkt;
|
||||
|
||||
double diff = priv->rebase_ts - priv->base_ts;
|
||||
mpkt.pts = PTS_ADD(mpkt.pts, diff);
|
||||
mpkt.dts = PTS_ADD(mpkt.dts, diff);
|
||||
|
||||
rst->max_out_pts = MPMAX(rst->max_out_pts, pkt->pts);
|
||||
|
||||
AVPacket avpkt;
|
||||
mp_set_av_packet(&avpkt, &mpkt, &rst->av_stream->time_base);
|
||||
|
||||
avpkt.stream_index = rst->av_stream->index;
|
||||
|
||||
if (avpkt.duration < 0 && rst->sh->type != STREAM_SUB)
|
||||
avpkt.duration = 0;
|
||||
|
||||
AVPacket *new_packet = av_packet_clone(&avpkt);
|
||||
if (!new_packet) {
|
||||
MP_ERR(priv, "Failed to allocate packet.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (av_interleaved_write_frame(priv->mux, new_packet) < 0)
|
||||
MP_ERR(priv, "Failed writing packet.\n");
|
||||
}
|
||||
|
||||
// Write all packets that currently can be written.
|
||||
static void mux_packets(struct mp_recorder_sink *rst, bool force)
|
||||
{
|
||||
struct mp_recorder *priv = rst->owner;
|
||||
if (!priv->muxing || !rst->num_packets)
|
||||
return;
|
||||
|
||||
int safe_count = 0;
|
||||
for (int n = 0; n < rst->num_packets; n++) {
|
||||
if (rst->packets[n]->keyframe)
|
||||
safe_count = n;
|
||||
}
|
||||
if (force)
|
||||
safe_count = rst->num_packets;
|
||||
|
||||
for (int n = 0; n < safe_count; n++) {
|
||||
mux_packet(rst, rst->packets[n]);
|
||||
talloc_free(rst->packets[n]);
|
||||
}
|
||||
|
||||
// Remove packets[0..safe_count]
|
||||
memmove(&rst->packets[0], &rst->packets[safe_count],
|
||||
(rst->num_packets - safe_count) * sizeof(rst->packets[0]));
|
||||
rst->num_packets -= safe_count;
|
||||
}
|
||||
|
||||
// If there was a discontinuity, check whether we can resume muxing (and from
|
||||
// where).
|
||||
static void check_restart(struct mp_recorder *priv)
|
||||
{
|
||||
if (priv->muxing)
|
||||
return;
|
||||
|
||||
double min_ts = INFINITY;
|
||||
double rebase_ts = 0;
|
||||
for (int n = 0; n < priv->num_streams; n++) {
|
||||
struct mp_recorder_sink *rst = priv->streams[n];
|
||||
int min_packets = rst->sh->type == STREAM_VIDEO ? QUEUE_MIN_PACKETS : 1;
|
||||
|
||||
rebase_ts = MPMAX(rebase_ts, rst->max_out_pts);
|
||||
|
||||
if (rst->num_packets < min_packets) {
|
||||
if (!rst->proper_eof && rst->sh->type != STREAM_SUB)
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < min_packets; i++)
|
||||
min_ts = MPMIN(min_ts, rst->packets[i]->pts);
|
||||
}
|
||||
|
||||
// Subtitle only stream (wait longer) or stream without any PTS (fuck it).
|
||||
if (!isfinite(min_ts))
|
||||
return;
|
||||
|
||||
priv->rebase_ts = rebase_ts;
|
||||
priv->base_ts = min_ts;
|
||||
|
||||
for (int n = 0; n < priv->num_streams; n++) {
|
||||
struct mp_recorder_sink *rst = priv->streams[n];
|
||||
rst->max_out_pts = min_ts;
|
||||
}
|
||||
|
||||
priv->muxing = true;
|
||||
|
||||
if (!priv->muxing_from_start)
|
||||
MP_WARN(priv, "Discontinuity at timestamp %f.\n", priv->rebase_ts);
|
||||
}
|
||||
|
||||
void mp_recorder_destroy(struct mp_recorder *priv)
|
||||
{
|
||||
if (priv->opened) {
|
||||
for (int n = 0; n < priv->num_streams; n++) {
|
||||
struct mp_recorder_sink *rst = priv->streams[n];
|
||||
if (!rst->proper_eof)
|
||||
continue;
|
||||
mux_packets(rst, true);
|
||||
}
|
||||
|
||||
if (av_write_trailer(priv->mux) < 0)
|
||||
MP_ERR(priv, "Writing trailer failed.\n");
|
||||
}
|
||||
|
||||
if (priv->mux) {
|
||||
if (avio_closep(&priv->mux->pb) < 0)
|
||||
MP_ERR(priv, "Closing file failed\n");
|
||||
|
||||
avformat_free_context(priv->mux);
|
||||
}
|
||||
|
||||
flush_packets(priv);
|
||||
talloc_free(priv);
|
||||
}
|
||||
|
||||
// This is called on a seek, or when recording was started mid-stream.
|
||||
void mp_recorder_mark_discontinuity(struct mp_recorder *priv)
|
||||
{
|
||||
flush_packets(priv);
|
||||
|
||||
for (int n = 0; n < priv->num_streams; n++) {
|
||||
struct mp_recorder_sink *rst = priv->streams[n];
|
||||
rst->discont = true;
|
||||
rst->proper_eof = false;
|
||||
}
|
||||
|
||||
priv->muxing = false;
|
||||
priv->muxing_from_start = false;
|
||||
}
|
||||
|
||||
// Get a stream for writing. The pointer is valid until mp_recorder is
|
||||
// destroyed. The stream is the index referencing the stream passed to
|
||||
// mp_recorder_create().
|
||||
struct mp_recorder_sink *mp_recorder_get_sink(struct mp_recorder *r, int stream)
|
||||
{
|
||||
assert(stream >= 0 && stream < r->num_streams);
|
||||
return r->streams[stream];
|
||||
}
|
||||
|
||||
// Pass a packet to the given stream. The function does not own the packet, but
|
||||
// can create a new reference to it if it needs to retain it. Can be NULL to
|
||||
// signal proper end of stream.
|
||||
void mp_recorder_feed_packet(struct mp_recorder_sink *rst,
|
||||
struct demux_packet *pkt)
|
||||
{
|
||||
struct mp_recorder *priv = rst->owner;
|
||||
|
||||
if (!pkt) {
|
||||
rst->proper_eof = true;
|
||||
check_restart(priv);
|
||||
mux_packets(rst, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pkt->dts == MP_NOPTS_VALUE && !priv->dts_warning) {
|
||||
// No, FFmpeg has no actually usable helpers to generate correct DTS.
|
||||
// No, FFmpeg doesn't tell us which formats need DTS at all.
|
||||
// No, we can not shut up the FFmpeg warning, which will follow.
|
||||
MP_WARN(priv, "Source stream misses DTS on at least some packets!\n"
|
||||
"If the target file format requires DTS, the written\n"
|
||||
"file will be invalid.\n");
|
||||
priv->dts_warning = true;
|
||||
}
|
||||
|
||||
if (rst->discont && !pkt->keyframe)
|
||||
return;
|
||||
rst->discont = false;
|
||||
|
||||
if (rst->num_packets >= QUEUE_MAX_PACKETS) {
|
||||
MP_ERR(priv, "Stream %d has too many queued packets; dropping.\n",
|
||||
rst->av_stream->index);
|
||||
return;
|
||||
}
|
||||
|
||||
pkt = demux_copy_packet(pkt);
|
||||
if (!pkt)
|
||||
return;
|
||||
MP_TARRAY_APPEND(rst, rst->packets, rst->num_packets, pkt);
|
||||
|
||||
check_restart(priv);
|
||||
mux_packets(rst, false);
|
||||
}
|
21
common/recorder.h
Normal file
21
common/recorder.h
Normal file
@ -0,0 +1,21 @@
|
||||
#ifndef MP_RECORDER_H_
|
||||
#define MP_RECORDER_H_
|
||||
|
||||
struct mp_recorder;
|
||||
struct mpv_global;
|
||||
struct demux_packet;
|
||||
struct sh_stream;
|
||||
struct mp_recorder_sink;
|
||||
|
||||
struct mp_recorder *mp_recorder_create(struct mpv_global *global,
|
||||
const char *target_file,
|
||||
struct sh_stream **streams,
|
||||
int num_streams);
|
||||
void mp_recorder_destroy(struct mp_recorder *r);
|
||||
void mp_recorder_mark_discontinuity(struct mp_recorder *r);
|
||||
|
||||
struct mp_recorder_sink *mp_recorder_get_sink(struct mp_recorder *r, int stream);
|
||||
void mp_recorder_feed_packet(struct mp_recorder_sink *s,
|
||||
struct demux_packet *pkt);
|
||||
|
||||
#endif
|
@ -681,6 +681,8 @@ const m_option_t mp_opts[] = {
|
||||
OPT_STRING("screenshot-template", screenshot_template, 0),
|
||||
OPT_STRING("screenshot-directory", screenshot_directory, 0),
|
||||
|
||||
OPT_STRING("record-file", record_file, M_OPT_FILE),
|
||||
|
||||
OPT_SUBSTRUCT("", input_opts, input_config, 0),
|
||||
|
||||
OPT_PRINT("list-protocols", stream_print_proto_list),
|
||||
|
@ -138,6 +138,7 @@ typedef struct MPOpts {
|
||||
|
||||
int untimed;
|
||||
char *stream_dump;
|
||||
char *record_file;
|
||||
int stop_playback_on_init_failure;
|
||||
int loop_times;
|
||||
int loop_file;
|
||||
|
@ -3549,6 +3549,26 @@ static int mp_property_cwd(void *ctx, struct m_property *prop,
|
||||
return M_PROPERTY_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
static int mp_property_record_file(void *ctx, struct m_property *prop,
|
||||
int action, void *arg)
|
||||
{
|
||||
struct MPContext *mpctx = ctx;
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
if (action == M_PROPERTY_SET) {
|
||||
char *new = *(char **)arg;
|
||||
if (!bstr_equals(bstr0(new), bstr0(opts->record_file))) {
|
||||
talloc_free(opts->record_file);
|
||||
opts->record_file = talloc_strdup(NULL, new);
|
||||
open_recorder(mpctx, false);
|
||||
// open_recorder() unsets it on failure.
|
||||
if (new && !opts->record_file)
|
||||
return M_PROPERTY_ERROR;
|
||||
}
|
||||
return M_PROPERTY_OK;
|
||||
}
|
||||
return mp_property_generic_option(mpctx, prop, action, arg);
|
||||
}
|
||||
|
||||
static int mp_property_protocols(void *ctx, struct m_property *prop,
|
||||
int action, void *arg)
|
||||
{
|
||||
@ -4020,6 +4040,8 @@ static const struct m_property mp_properties_base[] = {
|
||||
|
||||
{"working-directory", mp_property_cwd},
|
||||
|
||||
{"record-file", mp_property_record_file},
|
||||
|
||||
{"protocol-list", mp_property_protocols},
|
||||
{"decoder-list", mp_property_decoders},
|
||||
{"encoder-list", mp_property_encoders},
|
||||
|
@ -153,6 +153,9 @@ struct track {
|
||||
struct vo_chain *vo_c;
|
||||
struct ao_chain *ao_c;
|
||||
struct lavfi_pad *sink;
|
||||
|
||||
// For stream recording (remuxing mode).
|
||||
struct mp_recorder_sink *remux_sink;
|
||||
};
|
||||
|
||||
// Summarizes video filtering and output.
|
||||
@ -421,6 +424,8 @@ typedef struct MPContext {
|
||||
// playback rate. Used to avoid showing it multiple times.
|
||||
bool drop_message_shown;
|
||||
|
||||
struct mp_recorder *recorder;
|
||||
|
||||
char *cached_watch_later_configdir;
|
||||
|
||||
struct screenshot_ctx *screenshot_ctx;
|
||||
@ -506,6 +511,9 @@ void autoload_external_files(struct MPContext *mpctx);
|
||||
struct track *select_default_track(struct MPContext *mpctx, int order,
|
||||
enum stream_type type);
|
||||
void prefetch_next(struct MPContext *mpctx);
|
||||
void close_recorder(struct MPContext *mpctx);
|
||||
void close_recorder_and_error(struct MPContext *mpctx);
|
||||
void open_recorder(struct MPContext *mpctx, bool on_init);
|
||||
|
||||
// main.c
|
||||
int mp_initialize(struct MPContext *mpctx, char **argv);
|
||||
|
@ -40,6 +40,7 @@
|
||||
#include "options/m_property.h"
|
||||
#include "common/common.h"
|
||||
#include "common/encode.h"
|
||||
#include "common/recorder.h"
|
||||
#include "input/input.h"
|
||||
|
||||
#include "audio/audio.h"
|
||||
@ -469,6 +470,8 @@ void mp_switch_track_n(struct MPContext *mpctx, int order, enum stream_type type
|
||||
uninit_sub(mpctx, current);
|
||||
|
||||
if (current) {
|
||||
if (current->remux_sink)
|
||||
close_recorder_and_error(mpctx);
|
||||
current->selected = false;
|
||||
reselect_demux_stream(mpctx, current);
|
||||
}
|
||||
@ -1256,6 +1259,8 @@ reopen_file:
|
||||
if (mpctx->opts->pause)
|
||||
pause_player(mpctx);
|
||||
|
||||
open_recorder(mpctx, true);
|
||||
|
||||
playback_start = mp_time_sec();
|
||||
mpctx->error_playing = 0;
|
||||
while (!mpctx->stop_play)
|
||||
@ -1278,6 +1283,8 @@ terminate_playback:
|
||||
|
||||
mp_abort_playback_async(mpctx);
|
||||
|
||||
close_recorder(mpctx);
|
||||
|
||||
// time to uninit all, except global stuff:
|
||||
uninit_complex_filters(mpctx);
|
||||
uninit_audio_chain(mpctx);
|
||||
@ -1449,3 +1456,89 @@ void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e)
|
||||
mpctx->stop_play = PT_CURRENT_ENTRY;
|
||||
mp_wakeup_core(mpctx);
|
||||
}
|
||||
|
||||
static void set_track_recorder_sink(struct track *track,
|
||||
struct mp_recorder_sink *sink)
|
||||
{
|
||||
if (track->d_sub)
|
||||
sub_set_recorder_sink(track->d_sub, sink);
|
||||
if (track->d_video)
|
||||
track->d_video->recorder_sink = sink;
|
||||
if (track->d_audio)
|
||||
track->d_audio->recorder_sink = sink;
|
||||
track->remux_sink = sink;
|
||||
}
|
||||
|
||||
void close_recorder(struct MPContext *mpctx)
|
||||
{
|
||||
if (!mpctx->recorder)
|
||||
return;
|
||||
|
||||
for (int n = 0; n < mpctx->num_tracks; n++)
|
||||
set_track_recorder_sink(mpctx->tracks[n], NULL);
|
||||
|
||||
mp_recorder_destroy(mpctx->recorder);
|
||||
mpctx->recorder = NULL;
|
||||
}
|
||||
|
||||
// Like close_recorder(), but also unset the option. Intended for use on errors.
|
||||
void close_recorder_and_error(struct MPContext *mpctx)
|
||||
{
|
||||
close_recorder(mpctx);
|
||||
talloc_free(mpctx->opts->record_file);
|
||||
mpctx->opts->record_file = NULL;
|
||||
mp_notify_property(mpctx, "record-file");
|
||||
MP_ERR(mpctx, "Disabling stream recording.\n");
|
||||
}
|
||||
|
||||
void open_recorder(struct MPContext *mpctx, bool on_init)
|
||||
{
|
||||
if (!mpctx->playback_initialized)
|
||||
return;
|
||||
|
||||
close_recorder(mpctx);
|
||||
|
||||
char *target = mpctx->opts->record_file;
|
||||
if (!target || !target[0])
|
||||
return;
|
||||
|
||||
struct sh_stream **streams = NULL;
|
||||
int num_streams = 0;
|
||||
|
||||
for (int n = 0; n < mpctx->num_tracks; n++) {
|
||||
struct track *track = mpctx->tracks[n];
|
||||
if (track->stream && track->selected &&
|
||||
(track->d_sub || track->d_video || track->d_audio))
|
||||
{
|
||||
MP_TARRAY_APPEND(NULL, streams, num_streams, track->stream);
|
||||
}
|
||||
}
|
||||
|
||||
mpctx->recorder = mp_recorder_create(mpctx->global, mpctx->opts->record_file,
|
||||
streams, num_streams);
|
||||
|
||||
if (!mpctx->recorder) {
|
||||
talloc_free(streams);
|
||||
close_recorder_and_error(mpctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!on_init)
|
||||
mp_recorder_mark_discontinuity(mpctx->recorder);
|
||||
|
||||
int n_stream = 0;
|
||||
for (int n = 0; n < mpctx->num_tracks; n++) {
|
||||
struct track *track = mpctx->tracks[n];
|
||||
if (n_stream >= num_streams)
|
||||
break;
|
||||
// (We expect track->stream not to be reused on other tracks.)
|
||||
if (track->stream == streams[n_stream]) {
|
||||
set_track_recorder_sink(track,
|
||||
mp_recorder_get_sink(mpctx->recorder, n_stream));
|
||||
n_stream++;
|
||||
}
|
||||
}
|
||||
|
||||
talloc_free(streams);
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@
|
||||
#include "options/options.h"
|
||||
#include "common/common.h"
|
||||
#include "common/encode.h"
|
||||
#include "common/recorder.h"
|
||||
#include "options/m_config.h"
|
||||
#include "options/m_property.h"
|
||||
#include "common/playlist.h"
|
||||
@ -330,6 +331,8 @@ static void mp_seek(MPContext *mpctx, struct seek_params seek)
|
||||
clear_audio_output_buffers(mpctx);
|
||||
|
||||
reset_playback_state(mpctx);
|
||||
if (mpctx->recorder)
|
||||
mp_recorder_mark_discontinuity(mpctx->recorder);
|
||||
|
||||
/* Use the target time as "current position" for further relative
|
||||
* seeks etc until a new video frame has been decoded */
|
||||
|
@ -29,6 +29,7 @@
|
||||
#include "options/options.h"
|
||||
#include "common/global.h"
|
||||
#include "common/msg.h"
|
||||
#include "common/recorder.h"
|
||||
#include "osdep/threads.h"
|
||||
|
||||
extern const struct sd_functions sd_ass;
|
||||
@ -49,6 +50,8 @@ struct dec_sub {
|
||||
struct mpv_global *global;
|
||||
struct MPOpts *opts;
|
||||
|
||||
struct mp_recorder_sink *recorder_sink;
|
||||
|
||||
struct attachment_list *attachments;
|
||||
|
||||
struct sh_stream *sh;
|
||||
@ -240,6 +243,9 @@ bool sub_read_packets(struct dec_sub *sub, double video_pts)
|
||||
break;
|
||||
}
|
||||
|
||||
if (sub->recorder_sink)
|
||||
mp_recorder_feed_packet(sub->recorder_sink, pkt);
|
||||
|
||||
sub->last_pkt_pts = pkt->pts;
|
||||
|
||||
if (is_new_segment(sub, pkt)) {
|
||||
@ -323,3 +329,11 @@ int sub_control(struct dec_sub *sub, enum sd_ctrl cmd, void *arg)
|
||||
pthread_mutex_unlock(&sub->lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
void sub_set_recorder_sink(struct dec_sub *sub, struct mp_recorder_sink *sink)
|
||||
{
|
||||
pthread_mutex_lock(&sub->lock);
|
||||
sub->recorder_sink = sink;
|
||||
pthread_mutex_unlock(&sub->lock);
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
struct sh_stream;
|
||||
struct mpv_global;
|
||||
struct demux_packet;
|
||||
struct mp_recorder_sink;
|
||||
|
||||
struct dec_sub;
|
||||
struct sd;
|
||||
@ -40,6 +41,7 @@ void sub_get_bitmaps(struct dec_sub *sub, struct mp_osd_res dim, int format,
|
||||
char *sub_get_text(struct dec_sub *sub, double pts);
|
||||
void sub_reset(struct dec_sub *sub);
|
||||
void sub_select(struct dec_sub *sub, bool selected);
|
||||
void sub_set_recorder_sink(struct dec_sub *sub, struct mp_recorder_sink *sink);
|
||||
|
||||
int sub_control(struct dec_sub *sub, enum sd_ctrl cmd, void *arg);
|
||||
|
||||
|
@ -33,6 +33,7 @@
|
||||
#include "demux/packet.h"
|
||||
|
||||
#include "common/codecs.h"
|
||||
#include "common/recorder.h"
|
||||
|
||||
#include "video/out/vo.h"
|
||||
#include "video/csputils.h"
|
||||
@ -259,6 +260,12 @@ static bool send_packet(struct dec_video *d_video, struct demux_packet *packet)
|
||||
if (pkt_pts == MP_NOPTS_VALUE)
|
||||
d_video->has_broken_packet_pts = 1;
|
||||
|
||||
bool dts_replaced = false;
|
||||
if (packet && packet->dts == MP_NOPTS_VALUE && !d_video->codec->avi_dts) {
|
||||
packet->dts = packet->pts;
|
||||
dts_replaced = true;
|
||||
}
|
||||
|
||||
double pkt_pdts = pkt_pts == MP_NOPTS_VALUE ? pkt_dts : pkt_pts;
|
||||
if (pkt_pdts != MP_NOPTS_VALUE && d_video->first_packet_pdts == MP_NOPTS_VALUE)
|
||||
d_video->first_packet_pdts = pkt_pdts;
|
||||
@ -269,6 +276,10 @@ static bool send_packet(struct dec_video *d_video, struct demux_packet *packet)
|
||||
|
||||
MP_STATS(d_video, "end decode video");
|
||||
|
||||
// Stream recording can't deal with almost surely wrong fake DTS.
|
||||
if (dts_replaced)
|
||||
packet->dts = MP_NOPTS_VALUE;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -396,11 +407,6 @@ void video_work(struct dec_video *d_video)
|
||||
return;
|
||||
}
|
||||
|
||||
if (d_video->packet) {
|
||||
if (d_video->packet->dts == MP_NOPTS_VALUE && !d_video->codec->avi_dts)
|
||||
d_video->packet->dts = d_video->packet->pts;
|
||||
}
|
||||
|
||||
if (d_video->packet && d_video->packet->new_segment) {
|
||||
assert(!d_video->new_segment);
|
||||
d_video->new_segment = d_video->packet;
|
||||
@ -423,6 +429,9 @@ void video_work(struct dec_video *d_video)
|
||||
d_video->vd_driver->control(d_video, VDCTRL_SET_FRAMEDROP, &framedrop_type);
|
||||
|
||||
if (send_packet(d_video, d_video->packet)) {
|
||||
if (d_video->recorder_sink)
|
||||
mp_recorder_feed_packet(d_video->recorder_sink, d_video->packet);
|
||||
|
||||
talloc_free(d_video->packet);
|
||||
d_video->packet = NULL;
|
||||
}
|
||||
|
@ -42,6 +42,8 @@ struct dec_video {
|
||||
|
||||
int dropped_frames;
|
||||
|
||||
struct mp_recorder_sink *recorder_sink;
|
||||
|
||||
// Internal (shared with vd_lavc.c).
|
||||
|
||||
void *priv; // for free use by vd_driver
|
||||
|
@ -172,6 +172,7 @@ def build(ctx):
|
||||
( "common/tags.c" ),
|
||||
( "common/msg.c" ),
|
||||
( "common/playlist.c" ),
|
||||
( "common/recorder.c" ),
|
||||
( "common/version.c" ),
|
||||
|
||||
## Demuxers
|
||||
|
Loading…
Reference in New Issue
Block a user