mirror of
https://github.com/mpv-player/mpv.git
synced 2025-12-28 05:33:14 +00:00
Move mpvcore/player/ to player/
This commit is contained in:
471
player/audio.c
Normal file
471
player/audio.c
Normal file
@@ -0,0 +1,471 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include <math.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "talloc.h"
|
||||
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "mpvcore/options.h"
|
||||
#include "mpvcore/mp_common.h"
|
||||
|
||||
#include "audio/mixer.h"
|
||||
#include "audio/audio.h"
|
||||
#include "audio/audio_buffer.h"
|
||||
#include "audio/decode/dec_audio.h"
|
||||
#include "audio/filter/af.h"
|
||||
#include "audio/out/ao.h"
|
||||
#include "demux/demux.h"
|
||||
#include "video/decode/dec_video.h"
|
||||
|
||||
#include "mp_core.h"
|
||||
|
||||
static int build_afilter_chain(struct MPContext *mpctx)
|
||||
{
|
||||
struct dec_audio *d_audio = mpctx->d_audio;
|
||||
struct ao *ao = mpctx->ao;
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
|
||||
if (!d_audio)
|
||||
return 0;
|
||||
|
||||
struct mp_audio in_format;
|
||||
mp_audio_buffer_get_format(d_audio->decode_buffer, &in_format);
|
||||
|
||||
int new_srate;
|
||||
if (af_control_any_rev(d_audio->afilter, AF_CONTROL_SET_PLAYBACK_SPEED,
|
||||
&opts->playback_speed))
|
||||
new_srate = in_format.rate;
|
||||
else {
|
||||
new_srate = in_format.rate * opts->playback_speed;
|
||||
if (new_srate != ao->samplerate) {
|
||||
// limits are taken from libaf/af_resample.c
|
||||
if (new_srate < 8000)
|
||||
new_srate = 8000;
|
||||
if (new_srate > 192000)
|
||||
new_srate = 192000;
|
||||
opts->playback_speed = new_srate / (double)in_format.rate;
|
||||
}
|
||||
}
|
||||
return audio_init_filters(d_audio, new_srate,
|
||||
&ao->samplerate, &ao->channels, &ao->format);
|
||||
}
|
||||
|
||||
static int recreate_audio_filters(struct MPContext *mpctx)
|
||||
{
|
||||
assert(mpctx->d_audio);
|
||||
|
||||
// init audio filters:
|
||||
if (!build_afilter_chain(mpctx)) {
|
||||
MP_ERR(mpctx, "Couldn't find matching filter/ao format!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
mixer_reinit_audio(mpctx->mixer, mpctx->ao, mpctx->d_audio->afilter);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int reinit_audio_filters(struct MPContext *mpctx)
|
||||
{
|
||||
struct dec_audio *d_audio = mpctx->d_audio;
|
||||
if (!d_audio)
|
||||
return -2;
|
||||
|
||||
af_uninit(mpctx->d_audio->afilter);
|
||||
if (af_init(mpctx->d_audio->afilter) < 0)
|
||||
return -1;
|
||||
if (recreate_audio_filters(mpctx) < 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void reinit_audio_chain(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
struct sh_stream *sh = init_demux_stream(mpctx, STREAM_AUDIO);
|
||||
if (!sh) {
|
||||
uninit_player(mpctx, INITIALIZED_AO);
|
||||
goto no_audio;
|
||||
}
|
||||
|
||||
if (!(mpctx->initialized_flags & INITIALIZED_ACODEC)) {
|
||||
mpctx->initialized_flags |= INITIALIZED_ACODEC;
|
||||
assert(!mpctx->d_audio);
|
||||
mpctx->d_audio = talloc_zero(NULL, struct dec_audio);
|
||||
mpctx->d_audio->opts = opts;
|
||||
mpctx->d_audio->header = sh;
|
||||
if (!audio_init_best_codec(mpctx->d_audio, opts->audio_decoders))
|
||||
goto init_error;
|
||||
}
|
||||
assert(mpctx->d_audio);
|
||||
|
||||
struct mp_audio in_format;
|
||||
mp_audio_buffer_get_format(mpctx->d_audio->decode_buffer, &in_format);
|
||||
|
||||
int ao_srate = opts->force_srate;
|
||||
int ao_format = opts->audio_output_format;
|
||||
struct mp_chmap ao_channels = {0};
|
||||
if (mpctx->initialized_flags & INITIALIZED_AO) {
|
||||
ao_srate = mpctx->ao->samplerate;
|
||||
ao_format = mpctx->ao->format;
|
||||
ao_channels = mpctx->ao->channels;
|
||||
} else {
|
||||
// Automatic downmix
|
||||
if (mp_chmap_is_stereo(&opts->audio_output_channels) &&
|
||||
!mp_chmap_is_stereo(&in_format.channels))
|
||||
{
|
||||
mp_chmap_from_channels(&ao_channels, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine what the filter chain outputs. build_afilter_chain() also
|
||||
// needs this for testing whether playback speed is changed by resampling
|
||||
// or using a special filter.
|
||||
if (!audio_init_filters(mpctx->d_audio, // preliminary init
|
||||
// input:
|
||||
in_format.rate,
|
||||
// output:
|
||||
&ao_srate, &ao_channels, &ao_format)) {
|
||||
MP_ERR(mpctx, "Error at audio filter chain pre-init!\n");
|
||||
goto init_error;
|
||||
}
|
||||
|
||||
if (!(mpctx->initialized_flags & INITIALIZED_AO)) {
|
||||
mpctx->initialized_flags |= INITIALIZED_AO;
|
||||
mp_chmap_remove_useless_channels(&ao_channels,
|
||||
&opts->audio_output_channels);
|
||||
mpctx->ao = ao_init_best(mpctx->global, mpctx->input,
|
||||
mpctx->encode_lavc_ctx, ao_srate, ao_format,
|
||||
ao_channels);
|
||||
struct ao *ao = mpctx->ao;
|
||||
if (!ao) {
|
||||
MP_ERR(mpctx, "Could not open/initialize audio device -> no sound.\n");
|
||||
goto init_error;
|
||||
}
|
||||
|
||||
ao->buffer = mp_audio_buffer_create(ao);
|
||||
mp_audio_buffer_reinit_fmt(ao->buffer, ao->format, &ao->channels,
|
||||
ao->samplerate);
|
||||
|
||||
char *s = mp_audio_fmt_to_str(ao->samplerate, &ao->channels, ao->format);
|
||||
MP_INFO(mpctx, "AO: [%s] %s\n", ao->driver->name, s);
|
||||
talloc_free(s);
|
||||
MP_VERBOSE(mpctx, "AO: Description: %s\n", ao->driver->description);
|
||||
update_window_title(mpctx, true);
|
||||
}
|
||||
|
||||
if (recreate_audio_filters(mpctx) < 0)
|
||||
goto init_error;
|
||||
|
||||
mpctx->syncing_audio = true;
|
||||
return;
|
||||
|
||||
init_error:
|
||||
uninit_player(mpctx, INITIALIZED_ACODEC | INITIALIZED_AO);
|
||||
cleanup_demux_stream(mpctx, STREAM_AUDIO);
|
||||
no_audio:
|
||||
mpctx->current_track[STREAM_AUDIO] = NULL;
|
||||
MP_INFO(mpctx, "Audio: no audio\n");
|
||||
}
|
||||
|
||||
// Return pts value corresponding to the end point of audio written to the
|
||||
// ao so far.
|
||||
double written_audio_pts(struct MPContext *mpctx)
|
||||
{
|
||||
struct dec_audio *d_audio = mpctx->d_audio;
|
||||
if (!d_audio)
|
||||
return MP_NOPTS_VALUE;
|
||||
|
||||
struct mp_audio in_format;
|
||||
mp_audio_buffer_get_format(d_audio->decode_buffer, &in_format);
|
||||
|
||||
// first calculate the end pts of audio that has been output by decoder
|
||||
double a_pts = d_audio->pts;
|
||||
if (a_pts == MP_NOPTS_VALUE)
|
||||
return MP_NOPTS_VALUE;
|
||||
|
||||
// d_audio->pts is the timestamp of the latest input packet with
|
||||
// known pts that the decoder has decoded. d_audio->pts_bytes is
|
||||
// the amount of bytes the decoder has written after that timestamp.
|
||||
a_pts += d_audio->pts_offset / (double)in_format.rate;
|
||||
|
||||
// Now a_pts hopefully holds the pts for end of audio from decoder.
|
||||
// Subtract data in buffers between decoder and audio out.
|
||||
|
||||
// Decoded but not filtered
|
||||
a_pts -= mp_audio_buffer_seconds(d_audio->decode_buffer);
|
||||
|
||||
// Data buffered in audio filters, measured in seconds of "missing" output
|
||||
double buffered_output = af_calc_delay(d_audio->afilter);
|
||||
|
||||
// Data that was ready for ao but was buffered because ao didn't fully
|
||||
// accept everything to internal buffers yet
|
||||
buffered_output += mp_audio_buffer_seconds(mpctx->ao->buffer);
|
||||
|
||||
// Filters divide audio length by playback_speed, so multiply by it
|
||||
// to get the length in original units without speedup or slowdown
|
||||
a_pts -= buffered_output * mpctx->opts->playback_speed;
|
||||
|
||||
return a_pts + mpctx->video_offset;
|
||||
}
|
||||
|
||||
// Return pts value corresponding to currently playing audio.
|
||||
double playing_audio_pts(struct MPContext *mpctx)
|
||||
{
|
||||
double pts = written_audio_pts(mpctx);
|
||||
if (pts == MP_NOPTS_VALUE)
|
||||
return pts;
|
||||
return pts - mpctx->opts->playback_speed * ao_get_delay(mpctx->ao);
|
||||
}
|
||||
|
||||
static int write_to_ao(struct MPContext *mpctx, struct mp_audio *data, int flags,
|
||||
double pts)
|
||||
{
|
||||
if (mpctx->paused)
|
||||
return 0;
|
||||
struct ao *ao = mpctx->ao;
|
||||
ao->pts = pts;
|
||||
double real_samplerate = ao->samplerate / mpctx->opts->playback_speed;
|
||||
int played = ao_play(mpctx->ao, data->planes, data->samples, flags);
|
||||
assert(played <= data->samples);
|
||||
if (played > 0) {
|
||||
mpctx->shown_aframes += played;
|
||||
mpctx->delay += played / real_samplerate;
|
||||
// Keep correct pts for remaining data - could be used to flush
|
||||
// remaining buffer when closing ao.
|
||||
ao->pts += played / real_samplerate;
|
||||
return played;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int write_silence_to_ao(struct MPContext *mpctx, int samples, int flags,
|
||||
double pts)
|
||||
{
|
||||
struct mp_audio tmp = {0};
|
||||
mp_audio_buffer_get_format(mpctx->ao->buffer, &tmp);
|
||||
tmp.samples = samples;
|
||||
char *p = talloc_size(NULL, tmp.samples * tmp.sstride);
|
||||
for (int n = 0; n < tmp.num_planes; n++)
|
||||
tmp.planes[n] = p;
|
||||
mp_audio_fill_silence(&tmp, 0, tmp.samples);
|
||||
int r = write_to_ao(mpctx, &tmp, 0, pts);
|
||||
talloc_free(p);
|
||||
return r;
|
||||
}
|
||||
|
||||
#define ASYNC_PLAY_DONE -3
|
||||
static int audio_start_sync(struct MPContext *mpctx, int playsize)
|
||||
{
|
||||
struct ao *ao = mpctx->ao;
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
struct dec_audio *d_audio = mpctx->d_audio;
|
||||
int res;
|
||||
|
||||
assert(d_audio);
|
||||
|
||||
// Timing info may not be set without
|
||||
res = audio_decode(d_audio, ao->buffer, 1);
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
int samples;
|
||||
bool did_retry = false;
|
||||
double written_pts;
|
||||
double real_samplerate = ao->samplerate / opts->playback_speed;
|
||||
bool hrseek = mpctx->hrseek_active; // audio only hrseek
|
||||
mpctx->hrseek_active = false;
|
||||
while (1) {
|
||||
written_pts = written_audio_pts(mpctx);
|
||||
double ptsdiff;
|
||||
if (hrseek)
|
||||
ptsdiff = written_pts - mpctx->hrseek_pts;
|
||||
else
|
||||
ptsdiff = written_pts - mpctx->video_next_pts - mpctx->delay
|
||||
- mpctx->audio_delay;
|
||||
samples = ptsdiff * real_samplerate;
|
||||
|
||||
// ogg demuxers give packets without timing
|
||||
if (written_pts <= 1 && d_audio->pts == MP_NOPTS_VALUE) {
|
||||
if (!did_retry) {
|
||||
// Try to read more data to see packets that have pts
|
||||
res = audio_decode(d_audio, ao->buffer, ao->samplerate);
|
||||
if (res < 0)
|
||||
return res;
|
||||
did_retry = true;
|
||||
continue;
|
||||
}
|
||||
samples = 0;
|
||||
}
|
||||
|
||||
if (fabs(ptsdiff) > 300 || isnan(ptsdiff)) // pts reset or just broken?
|
||||
samples = 0;
|
||||
|
||||
if (samples > 0)
|
||||
break;
|
||||
|
||||
mpctx->syncing_audio = false;
|
||||
int skip_samples = -samples;
|
||||
int a = MPMIN(skip_samples, MPMAX(playsize, 2500));
|
||||
res = audio_decode(d_audio, ao->buffer, a);
|
||||
if (skip_samples <= mp_audio_buffer_samples(ao->buffer)) {
|
||||
mp_audio_buffer_skip(ao->buffer, skip_samples);
|
||||
if (res < 0)
|
||||
return res;
|
||||
return audio_decode(d_audio, ao->buffer, playsize);
|
||||
}
|
||||
mp_audio_buffer_clear(ao->buffer);
|
||||
if (res < 0)
|
||||
return res;
|
||||
}
|
||||
if (hrseek)
|
||||
// Don't add silence in audio-only case even if position is too late
|
||||
return 0;
|
||||
if (samples >= playsize) {
|
||||
/* This case could fall back to the one below with
|
||||
* samples = playsize, but then silence would keep accumulating
|
||||
* in ao->buffer if the AO accepts less data than it asks for
|
||||
* in playsize. */
|
||||
write_silence_to_ao(mpctx, playsize, 0,
|
||||
written_pts - samples / real_samplerate);
|
||||
return ASYNC_PLAY_DONE;
|
||||
}
|
||||
mpctx->syncing_audio = false;
|
||||
mp_audio_buffer_prepend_silence(ao->buffer, samples);
|
||||
return audio_decode(d_audio, ao->buffer, playsize);
|
||||
}
|
||||
|
||||
int fill_audio_out_buffers(struct MPContext *mpctx, double endpts)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
struct ao *ao = mpctx->ao;
|
||||
int playsize;
|
||||
int playflags = 0;
|
||||
bool audio_eof = false;
|
||||
bool signal_eof = false;
|
||||
bool partial_fill = false;
|
||||
struct dec_audio *d_audio = mpctx->d_audio;
|
||||
// Can't adjust the start of audio with spdif pass-through.
|
||||
bool modifiable_audio_format = !(ao->format & AF_FORMAT_SPECIAL_MASK);
|
||||
|
||||
assert(d_audio);
|
||||
|
||||
if (mpctx->paused)
|
||||
playsize = 1; // just initialize things (audio pts at least)
|
||||
else
|
||||
playsize = ao_get_space(ao);
|
||||
|
||||
// Coming here with hrseek_active still set means audio-only
|
||||
if (!mpctx->d_video || !mpctx->sync_audio_to_video)
|
||||
mpctx->syncing_audio = false;
|
||||
if (!opts->initial_audio_sync || !modifiable_audio_format) {
|
||||
mpctx->syncing_audio = false;
|
||||
mpctx->hrseek_active = false;
|
||||
}
|
||||
|
||||
int res;
|
||||
if (mpctx->syncing_audio || mpctx->hrseek_active)
|
||||
res = audio_start_sync(mpctx, playsize);
|
||||
else
|
||||
res = audio_decode(d_audio, ao->buffer, playsize);
|
||||
|
||||
if (res < 0) { // EOF, error or format change
|
||||
if (res == -2) {
|
||||
/* The format change isn't handled too gracefully. A more precise
|
||||
* implementation would require draining buffered old-format audio
|
||||
* while displaying video, then doing the output format switch.
|
||||
*/
|
||||
if (!mpctx->opts->gapless_audio)
|
||||
uninit_player(mpctx, INITIALIZED_AO);
|
||||
reinit_audio_chain(mpctx);
|
||||
return -1;
|
||||
} else if (res == ASYNC_PLAY_DONE)
|
||||
return 0;
|
||||
else if (demux_stream_eof(d_audio->header))
|
||||
audio_eof = true;
|
||||
}
|
||||
|
||||
if (endpts != MP_NOPTS_VALUE) {
|
||||
double samples = (endpts - written_audio_pts(mpctx) + mpctx->audio_delay)
|
||||
* ao->samplerate / opts->playback_speed;
|
||||
if (playsize > samples) {
|
||||
playsize = MPMAX(samples, 0);
|
||||
audio_eof = true;
|
||||
partial_fill = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (playsize > mp_audio_buffer_samples(ao->buffer)) {
|
||||
playsize = mp_audio_buffer_samples(ao->buffer);
|
||||
partial_fill = true;
|
||||
}
|
||||
if (!playsize)
|
||||
return partial_fill && audio_eof ? -2 : -partial_fill;
|
||||
|
||||
if (audio_eof && partial_fill) {
|
||||
if (opts->gapless_audio) {
|
||||
// With gapless audio, delay this to ao_uninit. There must be only
|
||||
// 1 final chunk, and that is handled when calling ao_uninit().
|
||||
signal_eof = true;
|
||||
} else {
|
||||
playflags |= AOPLAY_FINAL_CHUNK;
|
||||
}
|
||||
}
|
||||
|
||||
assert(ao->buffer_playable_samples <= mp_audio_buffer_samples(ao->buffer));
|
||||
|
||||
struct mp_audio data;
|
||||
mp_audio_buffer_peek(ao->buffer, &data);
|
||||
data.samples = MPMIN(data.samples, playsize);
|
||||
int played = write_to_ao(mpctx, &data, playflags, written_audio_pts(mpctx));
|
||||
ao->buffer_playable_samples = playsize - played;
|
||||
|
||||
if (played > 0) {
|
||||
mp_audio_buffer_skip(ao->buffer, played);
|
||||
} else if (!mpctx->paused && audio_eof && ao_get_delay(ao) < .04) {
|
||||
// Sanity check to avoid hanging in case current ao doesn't output
|
||||
// partial chunks and doesn't check for AOPLAY_FINAL_CHUNK
|
||||
signal_eof = true;
|
||||
}
|
||||
|
||||
return signal_eof ? -2 : -partial_fill;
|
||||
}
|
||||
|
||||
// Drop data queued for output, or which the AO is currently outputting.
|
||||
void clear_audio_output_buffers(struct MPContext *mpctx)
|
||||
{
|
||||
if (mpctx->ao) {
|
||||
ao_reset(mpctx->ao);
|
||||
mp_audio_buffer_clear(mpctx->ao->buffer);
|
||||
mpctx->ao->buffer_playable_samples = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Drop decoded data queued for filtering.
|
||||
void clear_audio_decode_buffers(struct MPContext *mpctx)
|
||||
{
|
||||
if (mpctx->d_audio)
|
||||
mp_audio_buffer_clear(mpctx->d_audio->decode_buffer);
|
||||
}
|
||||
3243
player/command.c
Normal file
3243
player/command.c
Normal file
File diff suppressed because it is too large
Load Diff
50
player/command.h
Normal file
50
player/command.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPLAYER_COMMAND_H
|
||||
#define MPLAYER_COMMAND_H
|
||||
|
||||
struct MPContext;
|
||||
struct mp_cmd;
|
||||
|
||||
void command_init(struct MPContext *mpctx);
|
||||
void command_uninit(struct MPContext *mpctx);
|
||||
|
||||
void run_command(struct MPContext *mpctx, struct mp_cmd *cmd);
|
||||
char *mp_property_expand_string(struct MPContext *mpctx, const char *str);
|
||||
void property_print_help(void);
|
||||
int mp_property_do(const char* name, int action, void* val,
|
||||
struct MPContext *mpctx);
|
||||
|
||||
const struct m_option *mp_get_property_list(void);
|
||||
|
||||
enum mp_event {
|
||||
MP_EVENT_NONE,
|
||||
MP_EVENT_TICK,
|
||||
MP_EVENT_PROPERTY, // char*, property that is changed
|
||||
MP_EVENT_TRACKS_CHANGED,
|
||||
MP_EVENT_START_FILE,
|
||||
MP_EVENT_END_FILE,
|
||||
};
|
||||
|
||||
void mp_notify(struct MPContext *mpctx, enum mp_event event, void *arg);
|
||||
void mp_notify_property(struct MPContext *mpctx, const char *property);
|
||||
|
||||
void mp_flush_events(struct MPContext *mpctx);
|
||||
|
||||
#endif /* MPLAYER_COMMAND_H */
|
||||
354
player/configfiles.c
Normal file
354
player/configfiles.c
Normal file
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include <libavutil/md5.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "talloc.h"
|
||||
|
||||
#include "osdep/io.h"
|
||||
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "mpvcore/path.h"
|
||||
#include "mpvcore/m_config.h"
|
||||
#include "mpvcore/parser-cfg.h"
|
||||
#include "mpvcore/playlist.h"
|
||||
#include "mpvcore/options.h"
|
||||
#include "mpvcore/m_property.h"
|
||||
|
||||
#include "stream/stream.h"
|
||||
|
||||
#include "mp_core.h"
|
||||
#include "command.h"
|
||||
|
||||
#define DEF_CONFIG "# Write your default config options here!\n\n\n"
|
||||
|
||||
bool mp_parse_cfgfiles(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
m_config_t *conf = mpctx->mconfig;
|
||||
char *conffile;
|
||||
if (!opts->load_config)
|
||||
return true;
|
||||
if (!m_config_parse_config_file(conf, MPLAYER_CONFDIR "/mpv.conf", 0) < 0)
|
||||
return false;
|
||||
mp_mk_config_dir(NULL);
|
||||
if ((conffile = mp_find_user_config_file("config")) == NULL)
|
||||
MP_ERR(mpctx, "mp_find_user_config_file(\"config\") problem\n");
|
||||
else {
|
||||
int fd = open(conffile, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666);
|
||||
if (fd != -1) {
|
||||
MP_INFO(mpctx, "Creating config file: %s\n", conffile);
|
||||
write(fd, DEF_CONFIG, sizeof(DEF_CONFIG) - 1);
|
||||
close(fd);
|
||||
}
|
||||
if (m_config_parse_config_file(conf, conffile, 0) < 0)
|
||||
return false;
|
||||
talloc_free(conffile);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set options file-local, and don't set them if the user set them via the
|
||||
// command line.
|
||||
#define FILE_LOCAL_FLAGS (M_SETOPT_BACKUP | M_SETOPT_PRESERVE_CMDLINE)
|
||||
|
||||
#define PROFILE_CFG_PROTOCOL "protocol."
|
||||
|
||||
void mp_load_per_protocol_config(m_config_t *conf, const char * const file)
|
||||
{
|
||||
char *str;
|
||||
char protocol[strlen(PROFILE_CFG_PROTOCOL) + strlen(file) + 1];
|
||||
m_profile_t *p;
|
||||
|
||||
/* does filename actually uses a protocol ? */
|
||||
if (!mp_is_url(bstr0(file)))
|
||||
return;
|
||||
str = strstr(file, "://");
|
||||
if (!str)
|
||||
return;
|
||||
|
||||
sprintf(protocol, "%s%s", PROFILE_CFG_PROTOCOL, file);
|
||||
protocol[strlen(PROFILE_CFG_PROTOCOL) + strlen(file) - strlen(str)] = '\0';
|
||||
p = m_config_get_profile0(conf, protocol);
|
||||
if (p) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO,
|
||||
"Loading protocol-related profile '%s'\n", protocol);
|
||||
m_config_set_profile(conf, p, FILE_LOCAL_FLAGS);
|
||||
}
|
||||
}
|
||||
|
||||
#define PROFILE_CFG_EXTENSION "extension."
|
||||
|
||||
void mp_load_per_extension_config(m_config_t *conf, const char * const file)
|
||||
{
|
||||
char *str;
|
||||
char extension[strlen(PROFILE_CFG_EXTENSION) + 8];
|
||||
m_profile_t *p;
|
||||
|
||||
/* does filename actually have an extension ? */
|
||||
str = strrchr(file, '.');
|
||||
if (!str)
|
||||
return;
|
||||
|
||||
sprintf(extension, PROFILE_CFG_EXTENSION);
|
||||
strncat(extension, ++str, 7);
|
||||
p = m_config_get_profile0(conf, extension);
|
||||
if (p) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO,
|
||||
"Loading extension-related profile '%s'\n", extension);
|
||||
m_config_set_profile(conf, p, FILE_LOCAL_FLAGS);
|
||||
}
|
||||
}
|
||||
|
||||
void mp_load_per_output_config(m_config_t *conf, char *cfg, char *out)
|
||||
{
|
||||
char profile[strlen(cfg) + strlen(out) + 1];
|
||||
m_profile_t *p;
|
||||
|
||||
if (!out && !out[0])
|
||||
return;
|
||||
|
||||
sprintf(profile, "%s%s", cfg, out);
|
||||
p = m_config_get_profile0(conf, profile);
|
||||
if (p) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO,
|
||||
"Loading extension-related profile '%s'\n", profile);
|
||||
m_config_set_profile(conf, p, FILE_LOCAL_FLAGS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load a config file (in file local mode)
|
||||
* @return 0 if file was not found, 1 otherwise
|
||||
*/
|
||||
static int try_load_config(m_config_t *conf, const char *file, int flags)
|
||||
{
|
||||
if (!mp_path_exists(file))
|
||||
return 0;
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO, "Loading config '%s'\n", file);
|
||||
m_config_parse_config_file(conf, file, flags);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mp_load_per_file_config(m_config_t *conf, const char * const file,
|
||||
bool search_file_dir)
|
||||
{
|
||||
char *confpath;
|
||||
char cfg[MP_PATH_MAX];
|
||||
const char *name;
|
||||
|
||||
if (strlen(file) > MP_PATH_MAX - 14) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_WARN, "Filename is too long, "
|
||||
"can not load file or directory specific config files\n");
|
||||
return;
|
||||
}
|
||||
sprintf(cfg, "%s.conf", file);
|
||||
|
||||
name = mp_basename(cfg);
|
||||
if (search_file_dir) {
|
||||
char dircfg[MP_PATH_MAX];
|
||||
strcpy(dircfg, cfg);
|
||||
strcpy(dircfg + (name - cfg), "mpv.conf");
|
||||
try_load_config(conf, dircfg, FILE_LOCAL_FLAGS);
|
||||
|
||||
if (try_load_config(conf, cfg, FILE_LOCAL_FLAGS))
|
||||
return;
|
||||
}
|
||||
|
||||
if ((confpath = mp_find_user_config_file(name)) != NULL) {
|
||||
try_load_config(conf, confpath, FILE_LOCAL_FLAGS);
|
||||
|
||||
talloc_free(confpath);
|
||||
}
|
||||
}
|
||||
|
||||
#define MP_WATCH_LATER_CONF "watch_later"
|
||||
|
||||
char *mp_get_playback_resume_config_filename(const char *fname,
|
||||
struct MPOpts *opts)
|
||||
{
|
||||
char *res = NULL;
|
||||
void *tmp = talloc_new(NULL);
|
||||
const char *realpath = fname;
|
||||
bstr bfname = bstr0(fname);
|
||||
if (!mp_is_url(bfname)) {
|
||||
char *cwd = mp_getcwd(tmp);
|
||||
if (!cwd)
|
||||
goto exit;
|
||||
realpath = mp_path_join(tmp, bstr0(cwd), bstr0(fname));
|
||||
}
|
||||
#if HAVE_DVDREAD || HAVE_DVDNAV
|
||||
if (bstr_startswith0(bfname, "dvd://"))
|
||||
realpath = talloc_asprintf(tmp, "%s - %s", realpath, dvd_device);
|
||||
#endif
|
||||
#if HAVE_LIBBLURAY
|
||||
if (bstr_startswith0(bfname, "br://") || bstr_startswith0(bfname, "bd://") ||
|
||||
bstr_startswith0(bfname, "bluray://"))
|
||||
realpath = talloc_asprintf(tmp, "%s - %s", realpath, bluray_device);
|
||||
#endif
|
||||
uint8_t md5[16];
|
||||
av_md5_sum(md5, realpath, strlen(realpath));
|
||||
char *conf = talloc_strdup(tmp, "");
|
||||
for (int i = 0; i < 16; i++)
|
||||
conf = talloc_asprintf_append(conf, "%02X", md5[i]);
|
||||
|
||||
conf = talloc_asprintf(tmp, "%s/%s", MP_WATCH_LATER_CONF, conf);
|
||||
|
||||
res = mp_find_user_config_file(conf);
|
||||
|
||||
exit:
|
||||
talloc_free(tmp);
|
||||
return res;
|
||||
}
|
||||
|
||||
static const char *backup_properties[] = {
|
||||
"osd-level",
|
||||
//"loop",
|
||||
"speed",
|
||||
"edition",
|
||||
"pause",
|
||||
"volume-restore-data",
|
||||
"audio-delay",
|
||||
//"balance",
|
||||
"fullscreen",
|
||||
"colormatrix",
|
||||
"colormatrix-input-range",
|
||||
"colormatrix-output-range",
|
||||
"ontop",
|
||||
"border",
|
||||
"gamma",
|
||||
"brightness",
|
||||
"contrast",
|
||||
"saturation",
|
||||
"hue",
|
||||
"deinterlace",
|
||||
"vf",
|
||||
"af",
|
||||
"panscan",
|
||||
"aid",
|
||||
"vid",
|
||||
"sid",
|
||||
"sub-delay",
|
||||
"sub-pos",
|
||||
"sub-visibility",
|
||||
"sub-scale",
|
||||
"ass-use-margins",
|
||||
"ass-vsfilter-aspect-compat",
|
||||
"ass-style-override",
|
||||
0
|
||||
};
|
||||
|
||||
// Should follow what parser-cfg.c does/needs
|
||||
static bool needs_config_quoting(const char *s)
|
||||
{
|
||||
for (int i = 0; s && s[i]; i++) {
|
||||
unsigned char c = s[i];
|
||||
if (!isprint(c) || isspace(c) || c == '#' || c == '\'' || c == '"')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void mp_write_watch_later_conf(struct MPContext *mpctx)
|
||||
{
|
||||
void *tmp = talloc_new(NULL);
|
||||
char *filename = mpctx->filename;
|
||||
if (!filename)
|
||||
goto exit;
|
||||
|
||||
double pos = get_current_time(mpctx);
|
||||
if (pos == MP_NOPTS_VALUE)
|
||||
goto exit;
|
||||
|
||||
mp_mk_config_dir(MP_WATCH_LATER_CONF);
|
||||
|
||||
char *conffile = mp_get_playback_resume_config_filename(mpctx->filename,
|
||||
mpctx->opts);
|
||||
talloc_steal(tmp, conffile);
|
||||
if (!conffile)
|
||||
goto exit;
|
||||
|
||||
MP_INFO(mpctx, "Saving state.\n");
|
||||
|
||||
FILE *file = fopen(conffile, "wb");
|
||||
if (!file)
|
||||
goto exit;
|
||||
fprintf(file, "start=%f\n", pos);
|
||||
for (int i = 0; backup_properties[i]; i++) {
|
||||
const char *pname = backup_properties[i];
|
||||
char *val = NULL;
|
||||
int r = mp_property_do(pname, M_PROPERTY_GET_STRING, &val, mpctx);
|
||||
if (r == M_PROPERTY_OK) {
|
||||
if (needs_config_quoting(val)) {
|
||||
// e.g. '%6%STRING'
|
||||
fprintf(file, "%s=%%%d%%%s\n", pname, (int)strlen(val), val);
|
||||
} else {
|
||||
fprintf(file, "%s=%s\n", pname, val);
|
||||
}
|
||||
}
|
||||
talloc_free(val);
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
exit:
|
||||
talloc_free(tmp);
|
||||
}
|
||||
|
||||
void mp_load_playback_resume(m_config_t *conf, const char *file)
|
||||
{
|
||||
char *fname = mp_get_playback_resume_config_filename(file, conf->optstruct);
|
||||
if (fname && mp_path_exists(fname)) {
|
||||
// Never apply the saved start position to following files
|
||||
m_config_backup_opt(conf, "start");
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO, "Resuming playback. This behavior can "
|
||||
"be disabled with --no-resume-playback.\n");
|
||||
try_load_config(conf, fname, M_SETOPT_PRESERVE_CMDLINE);
|
||||
unlink(fname);
|
||||
}
|
||||
talloc_free(fname);
|
||||
}
|
||||
|
||||
// Returns the first file that has a resume config.
|
||||
// Compared to hashing the playlist file or contents and managing separate
|
||||
// resume file for them, this is simpler, and also has the nice property
|
||||
// that appending to a playlist doesn't interfere with resuming (especially
|
||||
// if the playlist comes from the command line).
|
||||
struct playlist_entry *mp_resume_playlist(struct playlist *playlist,
|
||||
struct MPOpts *opts)
|
||||
{
|
||||
if (!opts->position_resume)
|
||||
return NULL;
|
||||
for (struct playlist_entry *e = playlist->first; e; e = e->next) {
|
||||
char *conf = mp_get_playback_resume_config_filename(e->filename, opts);
|
||||
bool exists = conf && mp_path_exists(conf);
|
||||
talloc_free(conf);
|
||||
if (exists)
|
||||
return e;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
251
player/dvdnav.c
Normal file
251
player/dvdnav.c
Normal file
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* This file is part of mpv.
|
||||
*
|
||||
* mpv is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with mpv. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <limits.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "mp_core.h"
|
||||
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "mpvcore/mp_common.h"
|
||||
#include "mpvcore/input/input.h"
|
||||
|
||||
#include "stream/stream_dvdnav.h"
|
||||
|
||||
#include "sub/dec_sub.h"
|
||||
#include "sub/osd.h"
|
||||
|
||||
#include "video/mp_image.h"
|
||||
#include "video/decode/dec_video.h"
|
||||
|
||||
struct mp_nav_state {
|
||||
struct mp_log *log;
|
||||
|
||||
int nav_still_frame;
|
||||
bool nav_eof;
|
||||
bool nav_menu;
|
||||
bool nav_draining;
|
||||
int hi_visible;
|
||||
int highlight[4]; // x0 y0 x1 y1
|
||||
int subsize[2];
|
||||
struct sub_bitmap *hi_elem;
|
||||
};
|
||||
|
||||
// Allocate state and enable navigation features. Must happen before
|
||||
// initializing cache, because the cache would read data. Since stream_dvdnav is
|
||||
// in a mode which skips all transitions on reading data (before enabling
|
||||
// navigation), this would skip some menu screens.
|
||||
void mp_nav_init(struct MPContext *mpctx)
|
||||
{
|
||||
assert(!mpctx->nav_state);
|
||||
|
||||
// dvdnav is interactive
|
||||
if (mpctx->encode_lavc_ctx)
|
||||
return;
|
||||
|
||||
struct mp_nav_cmd inp = {MP_NAV_CMD_ENABLE};
|
||||
if (stream_control(mpctx->stream, STREAM_CTRL_NAV_CMD, &inp) < 1)
|
||||
return;
|
||||
|
||||
mpctx->nav_state = talloc_zero(NULL, struct mp_nav_state);
|
||||
mpctx->nav_state->log = mp_log_new(mpctx->nav_state, mpctx->log, "dvdnav");
|
||||
|
||||
MP_VERBOSE(mpctx->nav_state, "enabling\n");
|
||||
|
||||
mp_input_enable_section(mpctx->input, "dvdnav", 0);
|
||||
mp_input_set_section_mouse_area(mpctx->input, "dvdnav-menu",
|
||||
INT_MIN, INT_MIN, INT_MAX, INT_MAX);
|
||||
}
|
||||
|
||||
void mp_nav_reset(struct MPContext *mpctx)
|
||||
{
|
||||
struct mp_nav_state *nav = mpctx->nav_state;
|
||||
if (!nav)
|
||||
return;
|
||||
struct mp_nav_cmd inp = {MP_NAV_CMD_RESUME};
|
||||
stream_control(mpctx->stream, STREAM_CTRL_NAV_CMD, &inp);
|
||||
nav->hi_visible = 0;
|
||||
nav->nav_menu = false;
|
||||
nav->nav_draining = false;
|
||||
nav->nav_still_frame = 0;
|
||||
mp_input_disable_section(mpctx->input, "dvdnav-menu");
|
||||
// Prevent demuxer init code to seek to the "start"
|
||||
mpctx->stream->start_pos = stream_tell(mpctx->stream);
|
||||
stream_control(mpctx->stream, STREAM_CTRL_RESUME_CACHE, NULL);
|
||||
}
|
||||
|
||||
void mp_nav_destroy(struct MPContext *mpctx)
|
||||
{
|
||||
if (!mpctx->nav_state)
|
||||
return;
|
||||
mp_input_disable_section(mpctx->input, "dvdnav");
|
||||
mp_input_disable_section(mpctx->input, "dvdnav-menu");
|
||||
talloc_free(mpctx->nav_state);
|
||||
mpctx->nav_state = NULL;
|
||||
}
|
||||
|
||||
void mp_nav_user_input(struct MPContext *mpctx, char *command)
|
||||
{
|
||||
struct mp_nav_state *nav = mpctx->nav_state;
|
||||
if (!nav)
|
||||
return;
|
||||
if (strcmp(command, "mouse_move") == 0) {
|
||||
struct mp_image_params vid = {0};
|
||||
if (mpctx->d_video)
|
||||
vid = mpctx->d_video->decoder_output;
|
||||
struct mp_nav_cmd inp = {MP_NAV_CMD_MOUSE_POS};
|
||||
int x, y;
|
||||
mp_input_get_mouse_pos(mpctx->input, &x, &y);
|
||||
osd_coords_to_video(mpctx->osd, vid.w, vid.h, &x, &y);
|
||||
inp.u.mouse_pos.x = x;
|
||||
inp.u.mouse_pos.y = y;
|
||||
stream_control(mpctx->stream, STREAM_CTRL_NAV_CMD, &inp);
|
||||
} else {
|
||||
struct mp_nav_cmd inp = {MP_NAV_CMD_MENU};
|
||||
inp.u.menu.action = command;
|
||||
stream_control(mpctx->stream, STREAM_CTRL_NAV_CMD, &inp);
|
||||
}
|
||||
}
|
||||
|
||||
void mp_handle_nav(struct MPContext *mpctx)
|
||||
{
|
||||
struct mp_nav_state *nav = mpctx->nav_state;
|
||||
if (!nav)
|
||||
return;
|
||||
while (1) {
|
||||
struct mp_nav_event *ev = NULL;
|
||||
stream_control(mpctx->stream, STREAM_CTRL_GET_NAV_EVENT, &ev);
|
||||
if (!ev)
|
||||
break;
|
||||
switch (ev->event) {
|
||||
case MP_NAV_EVENT_DRAIN: {
|
||||
nav->nav_draining = true;
|
||||
MP_VERBOSE(nav, "drain requested\n");
|
||||
break;
|
||||
}
|
||||
case MP_NAV_EVENT_RESET_ALL: {
|
||||
mpctx->stop_play = PT_RELOAD_DEMUXER;
|
||||
MP_VERBOSE(nav, "reload\n");
|
||||
break;
|
||||
}
|
||||
case MP_NAV_EVENT_RESET: {
|
||||
nav->nav_still_frame = 0;
|
||||
break;
|
||||
}
|
||||
case MP_NAV_EVENT_EOF:
|
||||
nav->nav_eof = true;
|
||||
break;
|
||||
case MP_NAV_EVENT_STILL_FRAME: {
|
||||
int len = ev->u.still_frame.seconds;
|
||||
MP_VERBOSE(nav, "wait for %d seconds\n", len);
|
||||
if (len > 0 && nav->nav_still_frame == 0)
|
||||
nav->nav_still_frame = len;
|
||||
break;
|
||||
}
|
||||
case MP_NAV_EVENT_MENU_MODE:
|
||||
nav->nav_menu = ev->u.menu_mode.enable;
|
||||
if (nav->nav_menu) {
|
||||
mp_input_enable_section(mpctx->input, "dvdnav-menu", 0);
|
||||
} else {
|
||||
mp_input_disable_section(mpctx->input, "dvdnav-menu");
|
||||
}
|
||||
break;
|
||||
case MP_NAV_EVENT_HIGHLIGHT:
|
||||
MP_VERBOSE(nav, "highlight: %d %d %d - %d %d\n",
|
||||
ev->u.highlight.display,
|
||||
ev->u.highlight.sx, ev->u.highlight.sy,
|
||||
ev->u.highlight.ex, ev->u.highlight.ey);
|
||||
nav->highlight[0] = ev->u.highlight.sx;
|
||||
nav->highlight[1] = ev->u.highlight.sy;
|
||||
nav->highlight[2] = ev->u.highlight.ex;
|
||||
nav->highlight[3] = ev->u.highlight.ey;
|
||||
nav->hi_visible = ev->u.highlight.display;
|
||||
mpctx->osd->highlight_priv = mpctx;
|
||||
osd_changed(mpctx->osd, OSDTYPE_NAV_HIGHLIGHT);
|
||||
break;
|
||||
default: ; // ignore
|
||||
}
|
||||
talloc_free(ev);
|
||||
}
|
||||
if (mpctx->stop_play == AT_END_OF_FILE) {
|
||||
if (nav->nav_still_frame > 0) {
|
||||
// gross hack
|
||||
mpctx->time_frame += nav->nav_still_frame;
|
||||
mpctx->playing_last_frame = true;
|
||||
nav->nav_still_frame = -2;
|
||||
} else if (nav->nav_still_frame == -2) {
|
||||
struct mp_nav_cmd inp = {MP_NAV_CMD_SKIP_STILL};
|
||||
stream_control(mpctx->stream, STREAM_CTRL_NAV_CMD, &inp);
|
||||
}
|
||||
}
|
||||
if (nav->nav_draining && mpctx->stop_play == AT_END_OF_FILE) {
|
||||
MP_VERBOSE(nav, "execute drain\n");
|
||||
struct mp_nav_cmd inp = {MP_NAV_CMD_DRAIN_OK};
|
||||
stream_control(mpctx->stream, STREAM_CTRL_NAV_CMD, &inp);
|
||||
nav->nav_draining = false;
|
||||
stream_control(mpctx->stream, STREAM_CTRL_RESUME_CACHE, NULL);
|
||||
}
|
||||
// E.g. keep displaying still frames
|
||||
if (mpctx->stop_play == AT_END_OF_FILE && !nav->nav_eof)
|
||||
mpctx->stop_play = KEEP_PLAYING;
|
||||
}
|
||||
|
||||
// Render "fake" highlights, because using actual dvd sub highlight elements
|
||||
// is too hard, and would require extra libavcodec to begin with.
|
||||
// Note: a proper solution would introduce something like
|
||||
// SD_CTRL_APPLY_DVDNAV, which would crop the vobsub frame,
|
||||
// and apply the current CLUT.
|
||||
void mp_nav_get_highlight(struct osd_state *osd, struct mp_osd_res res,
|
||||
struct sub_bitmaps *out_imgs)
|
||||
{
|
||||
struct MPContext *mpctx = osd->highlight_priv;
|
||||
struct mp_nav_state *nav = mpctx ? mpctx->nav_state : NULL;
|
||||
if (!nav)
|
||||
return;
|
||||
struct sub_bitmap *sub = nav->hi_elem;
|
||||
if (!sub)
|
||||
sub = talloc_zero(nav, struct sub_bitmap);
|
||||
|
||||
nav->hi_elem = sub;
|
||||
int sizes[2] = {0};
|
||||
if (mpctx->d_sub)
|
||||
sub_control(mpctx->d_sub, SD_CTRL_GET_RESOLUTION, sizes);
|
||||
if (sizes[0] < 1 || sizes[1] < 1) {
|
||||
struct mp_image_params vid = {0};
|
||||
if (mpctx->d_video)
|
||||
vid = mpctx->d_video->decoder_output;
|
||||
sizes[0] = vid.w;
|
||||
sizes[1] = vid.h;
|
||||
}
|
||||
if (sizes[0] < 1 || sizes[1] < 1)
|
||||
return;
|
||||
if (sizes[0] != nav->subsize[0] || sizes[1] != nav->subsize[1]) {
|
||||
talloc_free(sub->bitmap);
|
||||
sub->bitmap = talloc_array(sub, uint32_t, sizes[0] * sizes[1]);
|
||||
memset(sub->bitmap, 0x80, talloc_get_size(sub->bitmap));
|
||||
}
|
||||
|
||||
sub->x = nav->highlight[0];
|
||||
sub->y = nav->highlight[1];
|
||||
sub->w = MPCLAMP(nav->highlight[2] - sub->x, 0, sizes[0]);
|
||||
sub->h = MPCLAMP(nav->highlight[3] - sub->y, 0, sizes[1]);
|
||||
sub->stride = sub->w;
|
||||
out_imgs->format = SUBBITMAP_RGBA;
|
||||
out_imgs->parts = sub;
|
||||
out_imgs->num_parts = sub->w > 0 && sub->h > 0 && nav->hi_visible;
|
||||
osd_rescale_bitmaps(out_imgs, sizes[0], sizes[1], res, -1);
|
||||
}
|
||||
1423
player/loadfile.c
Normal file
1423
player/loadfile.c
Normal file
File diff suppressed because it is too large
Load Diff
98
player/lua/assdraw.lua
Normal file
98
player/lua/assdraw.lua
Normal file
@@ -0,0 +1,98 @@
|
||||
local ass_mt = {}
|
||||
ass_mt.__index = ass_mt
|
||||
|
||||
local function ass_new()
|
||||
return setmetatable({ scale = 4, text = "" }, ass_mt)
|
||||
end
|
||||
|
||||
function ass_mt.new_event(ass)
|
||||
-- osd_libass.c adds an event per line
|
||||
if #ass.text > 0 then
|
||||
ass.text = ass.text .. "\n"
|
||||
end
|
||||
end
|
||||
|
||||
function ass_mt.draw_start(ass)
|
||||
ass.text = string.format("%s{\\p%d}", ass.text, ass.scale)
|
||||
end
|
||||
|
||||
function ass_mt.draw_stop(ass)
|
||||
ass.text = ass.text .. "{\\p0}"
|
||||
end
|
||||
|
||||
function ass_mt.coord(ass, x, y)
|
||||
local scale = math.pow(2, ass.scale - 1)
|
||||
local ix = math.ceil(x * scale)
|
||||
local iy = math.ceil(y * scale)
|
||||
ass.text = string.format("%s %d %d", ass.text, ix, iy)
|
||||
end
|
||||
|
||||
function ass_mt.append(ass, s)
|
||||
ass.text = ass.text .. s
|
||||
end
|
||||
|
||||
function ass_mt.merge(ass1, ass2)
|
||||
ass1.text = ass1.text .. ass2.text
|
||||
end
|
||||
|
||||
function ass_mt.pos(ass, x, y)
|
||||
ass:append(string.format("{\\pos(%f,%f)}", x, y))
|
||||
end
|
||||
|
||||
function ass_mt.an(ass, an)
|
||||
ass:append(string.format("{\\an%d}", an))
|
||||
end
|
||||
|
||||
function ass_mt.move_to(ass, x, y)
|
||||
ass:append(" m")
|
||||
ass:coord(x, y)
|
||||
end
|
||||
|
||||
function ass_mt.line_to(ass, x, y)
|
||||
ass:append(" l")
|
||||
ass:coord(x, y)
|
||||
end
|
||||
|
||||
function ass_mt.bezier_curve(ass, x1, y1, x2, y2, x3, y3)
|
||||
ass:append(" b")
|
||||
ass:coord(x1, y1)
|
||||
ass:coord(x2, y2)
|
||||
ass:coord(x3, y3)
|
||||
end
|
||||
|
||||
|
||||
function ass_mt.rect_ccw(ass, x0, y0, x1, y1)
|
||||
ass:move_to(x0, y0)
|
||||
ass:line_to(x0, y1)
|
||||
ass:line_to(x1, y1)
|
||||
ass:line_to(x1, y0)
|
||||
end
|
||||
|
||||
function ass_mt.rect_cw(ass, x0, y0, x1, y1)
|
||||
ass:move_to(x0, y0)
|
||||
ass:line_to(x1, y0)
|
||||
ass:line_to(x1, y1)
|
||||
ass:line_to(x0, y1)
|
||||
end
|
||||
|
||||
function ass_mt.round_rect_cw(ass, x0, y0, x1, y1, r)
|
||||
ass:move_to(x0 + r, y0)
|
||||
ass:line_to(x1 - r, y0) -- top line
|
||||
if r > 0 then
|
||||
ass:bezier_curve(x1, y0, x1, y0, x1, y0 + r) -- top right corner
|
||||
end
|
||||
ass:line_to(x1, y1 - r) -- right line
|
||||
if r > 0 then
|
||||
ass:bezier_curve(x1, y1, x1, y1, x1 - r, y1) -- bottom right corner
|
||||
end
|
||||
ass:line_to(x0 + r, y1) -- bottom line
|
||||
if r > 0 then
|
||||
ass:bezier_curve(x0, y1, x0, y1, x0, y1 - r) -- bottom left corner
|
||||
end
|
||||
ass:line_to(x0, y0 + r) -- left line
|
||||
if r > 0 then
|
||||
ass:bezier_curve(x0, y0, x0, y0, x0 + r, y0) -- top left corner
|
||||
end
|
||||
end
|
||||
|
||||
return {ass_new = ass_new}
|
||||
82
player/lua/defaults.lua
Normal file
82
player/lua/defaults.lua
Normal file
@@ -0,0 +1,82 @@
|
||||
|
||||
local callbacks = {}
|
||||
-- each script has its own section, so that they don't conflict
|
||||
local default_section = "input_" .. mp.script_name
|
||||
|
||||
-- Set the list of key bindings. These will override the user's bindings, so
|
||||
-- you should use this sparingly.
|
||||
-- A call to this function will remove all bindings previously set with this
|
||||
-- function. For example, set_key_bindings({}) would remove all script defined
|
||||
-- key bindings.
|
||||
-- Note: the bindings are not active by default. Use enable_key_bindings().
|
||||
--
|
||||
-- list is an array of key bindings, where each entry is an array as follow:
|
||||
-- {key, callback}
|
||||
-- {key, callback, callback_down}
|
||||
-- key is the key string as used in input.conf, like "ctrl+a"
|
||||
-- callback is a Lua function that is called when the key binding is used.
|
||||
-- callback_down can be given too, and is called when a mouse button is pressed
|
||||
-- if the key is a mouse button. (The normal callback will be for mouse button
|
||||
-- down.)
|
||||
--
|
||||
-- callback can be a string too, in which case the following will be added like
|
||||
-- an input.conf line: key .. " " .. callback
|
||||
-- (And callback_down is ignored.)
|
||||
function mp.set_key_bindings(list, section)
|
||||
local cfg = ""
|
||||
for i = 1, #list do
|
||||
local entry = list[i]
|
||||
local key = entry[1]
|
||||
local cb = entry[2]
|
||||
local cb_down = entry[3]
|
||||
if type(cb) == "function" then
|
||||
callbacks[#callbacks + 1] = {press=cb, before_press=cb_down}
|
||||
cfg = cfg .. key .. " script_dispatch " .. mp.script_name
|
||||
.. " " .. #callbacks .. "\n"
|
||||
else
|
||||
cfg = cfg .. key .. " " .. cb .. "\n"
|
||||
end
|
||||
end
|
||||
mp.input_define_section(section or default_section, cfg)
|
||||
end
|
||||
|
||||
function mp.enable_key_bindings(section, flags)
|
||||
mp.input_enable_section(section or default_section, flags)
|
||||
end
|
||||
|
||||
function mp.disable_key_bindings(section)
|
||||
mp.input_disable_section(section or default_section)
|
||||
end
|
||||
|
||||
function mp.set_mouse_area(x0, y0, x1, y1, section)
|
||||
mp.input_set_section_mouse_area(section or default_section, x0, y0, x1, y1)
|
||||
end
|
||||
|
||||
-- called by C on script_dispatch input command
|
||||
function mp_script_dispatch(id, event)
|
||||
local cb = callbacks[id]
|
||||
if cb then
|
||||
if event == "press" and cb.press then
|
||||
cb.press()
|
||||
elseif event == "keyup_follows" and cb.before_press then
|
||||
cb.before_press()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
mp.msg = {
|
||||
log = mp.log,
|
||||
fatal = function(...) return mp.log("fatal", ...) end,
|
||||
error = function(...) return mp.log("error", ...) end,
|
||||
warn = function(...) return mp.log("warn", ...) end,
|
||||
info = function(...) return mp.log("info", ...) end,
|
||||
verbose = function(...) return mp.log("verbose", ...) end,
|
||||
debug = function(...) return mp.log("debug", ...) end,
|
||||
}
|
||||
|
||||
_G.print = mp.msg.info
|
||||
|
||||
package.loaded["mp"] = mp
|
||||
package.loaded["mp.msg"] = mp.msg
|
||||
|
||||
return {}
|
||||
1288
player/lua/osc.lua
Normal file
1288
player/lua/osc.lua
Normal file
File diff suppressed because it is too large
Load Diff
450
player/main.c
Normal file
450
player/main.c
Normal file
@@ -0,0 +1,450 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <math.h>
|
||||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "talloc.h"
|
||||
|
||||
#include "osdep/io.h"
|
||||
#include "osdep/getch2.h"
|
||||
#include "osdep/priority.h"
|
||||
#include "osdep/timer.h"
|
||||
|
||||
#include "mpvcore/av_log.h"
|
||||
#include "mpvcore/codecs.h"
|
||||
#include "mpvcore/cpudetect.h"
|
||||
#include "mpvcore/encode.h"
|
||||
#include "mpvcore/m_config.h"
|
||||
#include "mpvcore/m_option.h"
|
||||
#include "mpvcore/m_property.h"
|
||||
#include "mpvcore/mp_common.h"
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "mpvcore/mpv_global.h"
|
||||
#include "mpvcore/resolve.h"
|
||||
#include "mpvcore/parser-cfg.h"
|
||||
#include "mpvcore/parser-mpcmd.h"
|
||||
#include "mpvcore/playlist.h"
|
||||
#include "mpvcore/playlist_parser.h"
|
||||
#include "mpvcore/options.h"
|
||||
#include "mpvcore/input/input.h"
|
||||
|
||||
#include "audio/decode/dec_audio.h"
|
||||
#include "audio/out/ao.h"
|
||||
#include "audio/mixer.h"
|
||||
#include "demux/demux.h"
|
||||
#include "stream/stream.h"
|
||||
#include "sub/ass_mp.h"
|
||||
#include "sub/osd.h"
|
||||
#include "video/decode/dec_video.h"
|
||||
#include "video/out/vo.h"
|
||||
|
||||
#include "mp_core.h"
|
||||
#include "mp_lua.h"
|
||||
#include "command.h"
|
||||
#include "screenshot.h"
|
||||
|
||||
#if HAVE_X11
|
||||
#include "video/out/x11_common.h"
|
||||
#endif
|
||||
|
||||
#if HAVE_COCOA
|
||||
#include "osdep/macosx_application.h"
|
||||
#endif
|
||||
|
||||
#ifdef PTW32_STATIC_LIB
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
#if defined(__MINGW32__) || defined(__CYGWIN__)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
const char mp_help_text[] = _(
|
||||
"Usage: mpv [options] [url|path/]filename\n"
|
||||
"\n"
|
||||
"Basic options:\n"
|
||||
" --start=<time> seek to given (percent, seconds, or hh:mm:ss) position\n"
|
||||
" --no-audio do not play sound\n"
|
||||
" --no-video do not play video\n"
|
||||
" --fs fullscreen playback\n"
|
||||
" --sub=<file> specify subtitle file to use\n"
|
||||
" --playlist=<file> specify playlist file\n"
|
||||
"\n"
|
||||
" --list-options list all mpv options\n"
|
||||
"\n");
|
||||
|
||||
void mp_print_version(int always)
|
||||
{
|
||||
int v = always ? MSGL_INFO : MSGL_V;
|
||||
mp_msg(MSGT_CPLAYER, v,
|
||||
"%s (C) 2000-2013 mpv/MPlayer/mplayer2 projects\n built on %s\n", mplayer_version, mplayer_builddate);
|
||||
print_libav_versions(v);
|
||||
mp_msg(MSGT_CPLAYER, v, "\n");
|
||||
}
|
||||
|
||||
static MP_NORETURN void exit_player(struct MPContext *mpctx,
|
||||
enum exit_reason how)
|
||||
{
|
||||
int rc;
|
||||
uninit_player(mpctx, INITIALIZED_ALL);
|
||||
|
||||
#if HAVE_ENCODING
|
||||
encode_lavc_finish(mpctx->encode_lavc_ctx);
|
||||
encode_lavc_free(mpctx->encode_lavc_ctx);
|
||||
#endif
|
||||
|
||||
mpctx->encode_lavc_ctx = NULL;
|
||||
|
||||
#if HAVE_LUA
|
||||
mp_lua_uninit(mpctx);
|
||||
#endif
|
||||
|
||||
#if defined(__MINGW32__)
|
||||
timeEndPeriod(1);
|
||||
#endif
|
||||
|
||||
#if HAVE_COCOA
|
||||
cocoa_set_input_context(NULL);
|
||||
#endif
|
||||
|
||||
command_uninit(mpctx);
|
||||
|
||||
mp_input_uninit(mpctx->input);
|
||||
|
||||
osd_free(mpctx->osd);
|
||||
|
||||
#if HAVE_LIBASS
|
||||
ass_library_done(mpctx->ass_library);
|
||||
mpctx->ass_library = NULL;
|
||||
#endif
|
||||
|
||||
getch2_disable();
|
||||
|
||||
if (how != EXIT_NONE) {
|
||||
const char *reason;
|
||||
switch (how) {
|
||||
case EXIT_SOMENOTPLAYED:
|
||||
case EXIT_PLAYED:
|
||||
reason = "End of file";
|
||||
break;
|
||||
case EXIT_NOTPLAYED:
|
||||
reason = "No files played";
|
||||
break;
|
||||
case EXIT_ERROR:
|
||||
reason = "Fatal error";
|
||||
break;
|
||||
default:
|
||||
reason = "Quit";
|
||||
}
|
||||
MP_INFO(mpctx, "\nExiting... (%s)\n", reason);
|
||||
}
|
||||
|
||||
if (mpctx->has_quit_custom_rc) {
|
||||
rc = mpctx->quit_custom_rc;
|
||||
} else {
|
||||
switch (how) {
|
||||
case EXIT_ERROR:
|
||||
rc = 1; break;
|
||||
case EXIT_NOTPLAYED:
|
||||
rc = 2; break;
|
||||
case EXIT_SOMENOTPLAYED:
|
||||
rc = 3; break;
|
||||
default:
|
||||
rc = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// must be last since e.g. mp_msg uses option values
|
||||
// that will be freed by this.
|
||||
|
||||
mp_msg_uninit(mpctx->global);
|
||||
talloc_free(mpctx);
|
||||
|
||||
#if HAVE_COCOA
|
||||
terminate_cocoa_application();
|
||||
// never reach here:
|
||||
// terminate calls exit itself, just silence compiler warning
|
||||
exit(0);
|
||||
#else
|
||||
exit(rc);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool handle_help_options(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
int opt_exit = 0;
|
||||
if (opts->audio_decoders && strcmp(opts->audio_decoders, "help") == 0) {
|
||||
struct mp_decoder_list *list = audio_decoder_list();
|
||||
mp_print_decoders(MSGT_CPLAYER, MSGL_INFO, "Audio decoders:", list);
|
||||
talloc_free(list);
|
||||
opt_exit = 1;
|
||||
}
|
||||
if (opts->video_decoders && strcmp(opts->video_decoders, "help") == 0) {
|
||||
struct mp_decoder_list *list = video_decoder_list();
|
||||
mp_print_decoders(MSGT_CPLAYER, MSGL_INFO, "Video decoders:", list);
|
||||
talloc_free(list);
|
||||
opt_exit = 1;
|
||||
}
|
||||
#if HAVE_X11
|
||||
if (opts->vo.fstype_list && strcmp(opts->vo.fstype_list[0], "help") == 0) {
|
||||
fstype_help();
|
||||
mp_msg(MSGT_FIXME, MSGL_FIXME, "\n");
|
||||
opt_exit = 1;
|
||||
}
|
||||
#endif
|
||||
if ((opts->demuxer_name && strcmp(opts->demuxer_name, "help") == 0) ||
|
||||
(opts->audio_demuxer_name && strcmp(opts->audio_demuxer_name, "help") == 0) ||
|
||||
(opts->sub_demuxer_name && strcmp(opts->sub_demuxer_name, "help") == 0)) {
|
||||
demuxer_help();
|
||||
MP_INFO(mpctx, "\n");
|
||||
opt_exit = 1;
|
||||
}
|
||||
if (opts->list_properties) {
|
||||
property_print_help();
|
||||
opt_exit = 1;
|
||||
}
|
||||
#if HAVE_ENCODING
|
||||
if (encode_lavc_showhelp(mpctx->opts))
|
||||
opt_exit = 1;
|
||||
#endif
|
||||
return opt_exit;
|
||||
}
|
||||
|
||||
#ifdef PTW32_STATIC_LIB
|
||||
static void detach_ptw32(void)
|
||||
{
|
||||
pthread_win32_thread_detach_np();
|
||||
pthread_win32_process_detach_np();
|
||||
}
|
||||
#endif
|
||||
|
||||
static void osdep_preinit(int *p_argc, char ***p_argv)
|
||||
{
|
||||
char *enable_talloc = getenv("MPV_LEAK_REPORT");
|
||||
if (*p_argc > 1 && (strcmp((*p_argv)[1], "-leak-report") == 0 ||
|
||||
strcmp((*p_argv)[1], "--leak-report") == 0))
|
||||
enable_talloc = "1";
|
||||
if (enable_talloc && strcmp(enable_talloc, "1") == 0)
|
||||
talloc_enable_leak_report();
|
||||
|
||||
#ifdef __MINGW32__
|
||||
mp_get_converted_argv(p_argc, p_argv);
|
||||
#endif
|
||||
|
||||
#ifdef PTW32_STATIC_LIB
|
||||
pthread_win32_process_attach_np();
|
||||
pthread_win32_thread_attach_np();
|
||||
atexit(detach_ptw32);
|
||||
#endif
|
||||
|
||||
#if defined(__MINGW32__) || defined(__CYGWIN__)
|
||||
// stop Windows from showing all kinds of annoying error dialogs
|
||||
SetErrorMode(0x8003);
|
||||
#endif
|
||||
|
||||
load_termcap(NULL); // load key-codes
|
||||
|
||||
mp_time_init();
|
||||
}
|
||||
|
||||
static int read_keys(void *ctx, int fd)
|
||||
{
|
||||
if (getch2(ctx))
|
||||
return MP_INPUT_NOTHING;
|
||||
return MP_INPUT_DEAD;
|
||||
}
|
||||
|
||||
static void init_input(struct MPContext *mpctx)
|
||||
{
|
||||
mpctx->input = mp_input_init(mpctx->global);
|
||||
if (mpctx->opts->slave_mode)
|
||||
mp_input_add_cmd_fd(mpctx->input, 0, USE_FD0_CMD_SELECT, MP_INPUT_SLAVE_CMD_FUNC, NULL);
|
||||
else if (mpctx->opts->consolecontrols)
|
||||
mp_input_add_key_fd(mpctx->input, 0, 1, read_keys, NULL, mpctx->input);
|
||||
// Set the libstream interrupt callback
|
||||
stream_set_interrupt_callback(mp_input_check_interrupt, mpctx->input);
|
||||
|
||||
#if HAVE_COCOA
|
||||
cocoa_set_input_context(mpctx->input);
|
||||
#endif
|
||||
}
|
||||
|
||||
static int cfg_include(struct m_config *conf, char *filename, int flags)
|
||||
{
|
||||
return m_config_parse_config_file(conf, filename, flags);
|
||||
}
|
||||
|
||||
static int mpv_main(int argc, char *argv[])
|
||||
{
|
||||
osdep_preinit(&argc, &argv);
|
||||
|
||||
if (argc >= 1) {
|
||||
argc--;
|
||||
argv++;
|
||||
}
|
||||
|
||||
struct MPContext *mpctx = talloc(NULL, MPContext);
|
||||
*mpctx = (struct MPContext){
|
||||
.last_dvb_step = 1,
|
||||
.terminal_osd_text = talloc_strdup(mpctx, ""),
|
||||
.playlist = talloc_struct(mpctx, struct playlist, {0}),
|
||||
};
|
||||
|
||||
// Create the config context and register the options
|
||||
mpctx->mconfig = m_config_new(mpctx, sizeof(struct MPOpts),
|
||||
&mp_default_opts, mp_opts);
|
||||
mpctx->opts = mpctx->mconfig->optstruct;
|
||||
mpctx->mconfig->includefunc = cfg_include;
|
||||
mpctx->mconfig->use_profiles = true;
|
||||
mpctx->mconfig->is_toplevel = true;
|
||||
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
|
||||
|
||||
mpctx->global = talloc_zero(mpctx, struct mpv_global);
|
||||
mpctx->global->opts = opts;
|
||||
|
||||
// Nothing must call mp_msg() before this
|
||||
mp_msg_init(mpctx->global);
|
||||
mpctx->log = mp_log_new(mpctx, mpctx->global->log, "!cplayer");
|
||||
|
||||
init_libav();
|
||||
GetCpuCaps(&gCpuCaps);
|
||||
screenshot_init(mpctx);
|
||||
mpctx->mixer = mixer_init(mpctx, opts);
|
||||
command_init(mpctx);
|
||||
|
||||
// Preparse the command line
|
||||
m_config_preparse_command_line(mpctx->mconfig, argc, argv);
|
||||
|
||||
mp_print_version(false);
|
||||
|
||||
if (!mp_parse_cfgfiles(mpctx))
|
||||
exit_player(mpctx, EXIT_ERROR);
|
||||
|
||||
int r = m_config_parse_mp_command_line(mpctx->mconfig, mpctx->playlist,
|
||||
argc, argv);
|
||||
if (r < 0) {
|
||||
if (r <= M_OPT_EXIT) {
|
||||
exit_player(mpctx, EXIT_NONE);
|
||||
} else {
|
||||
exit_player(mpctx, EXIT_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
if (handle_help_options(mpctx))
|
||||
exit_player(mpctx, EXIT_NONE);
|
||||
|
||||
MP_VERBOSE(mpctx, "Configuration: " CONFIGURATION "\n");
|
||||
MP_VERBOSE(mpctx, "Command line:");
|
||||
for (int i = 0; i < argc; i++)
|
||||
MP_VERBOSE(mpctx, " '%s'", argv[i]);
|
||||
MP_VERBOSE(mpctx, "\n");
|
||||
|
||||
if (!mpctx->playlist->first && !opts->player_idle_mode) {
|
||||
mp_print_version(true);
|
||||
MP_INFO(mpctx, "%s", mp_help_text);
|
||||
exit_player(mpctx, EXIT_NONE);
|
||||
}
|
||||
|
||||
#if HAVE_PRIORITY
|
||||
set_priority();
|
||||
#endif
|
||||
|
||||
init_input(mpctx);
|
||||
|
||||
#if HAVE_ENCODING
|
||||
if (opts->encode_output.file && *opts->encode_output.file) {
|
||||
mpctx->encode_lavc_ctx = encode_lavc_init(&opts->encode_output);
|
||||
if(!mpctx->encode_lavc_ctx) {
|
||||
mp_msg(MSGT_VO, MSGL_INFO, "Encoding initialization failed.");
|
||||
exit_player(mpctx, EXIT_ERROR);
|
||||
}
|
||||
m_config_set_option0(mpctx->mconfig, "vo", "lavc");
|
||||
m_config_set_option0(mpctx->mconfig, "ao", "lavc");
|
||||
m_config_set_option0(mpctx->mconfig, "fixed-vo", "yes");
|
||||
m_config_set_option0(mpctx->mconfig, "force-window", "no");
|
||||
m_config_set_option0(mpctx->mconfig, "gapless-audio", "yes");
|
||||
mp_input_enable_section(mpctx->input, "encode", MP_INPUT_EXCLUSIVE);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (opts->consolecontrols)
|
||||
getch2_enable();
|
||||
|
||||
#if HAVE_LIBASS
|
||||
mpctx->ass_library = mp_ass_init(opts);
|
||||
#else
|
||||
MP_WARN(mpctx, "Compiled without libass.\n");
|
||||
MP_WARN(mpctx, "There will be no OSD and no text subtitles.\n");
|
||||
#endif
|
||||
|
||||
mpctx->osd = osd_create(opts);
|
||||
|
||||
if (opts->force_vo) {
|
||||
opts->fixed_vo = 1;
|
||||
mpctx->video_out = init_best_video_out(mpctx->global, mpctx->input,
|
||||
mpctx->encode_lavc_ctx);
|
||||
if (!mpctx->video_out) {
|
||||
MP_FATAL(mpctx, "Error opening/initializing "
|
||||
"the selected video_out (-vo) device.\n");
|
||||
exit_player(mpctx, EXIT_ERROR);
|
||||
}
|
||||
mpctx->mouse_cursor_visible = true;
|
||||
mpctx->initialized_flags |= INITIALIZED_VO;
|
||||
}
|
||||
|
||||
#if HAVE_LUA
|
||||
// Lua user scripts can call arbitrary functions. Load them at a point
|
||||
// where this is safe.
|
||||
mp_lua_init(mpctx);
|
||||
#endif
|
||||
|
||||
if (opts->shuffle)
|
||||
playlist_shuffle(mpctx->playlist);
|
||||
|
||||
if (opts->merge_files)
|
||||
merge_playlist_files(mpctx->playlist);
|
||||
|
||||
mpctx->playlist->current = mp_resume_playlist(mpctx->playlist, opts);
|
||||
if (!mpctx->playlist->current)
|
||||
mpctx->playlist->current = mpctx->playlist->first;
|
||||
|
||||
mp_play_files(mpctx);
|
||||
|
||||
exit_player(mpctx, mpctx->stop_play == PT_QUIT ? EXIT_QUIT : mpctx->quit_player_rc);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
#if HAVE_COCOA
|
||||
return cocoa_main(mpv_main, argc, argv);
|
||||
#else
|
||||
return mpv_main(argc, argv);
|
||||
#endif
|
||||
}
|
||||
210
player/misc.c
Normal file
210
player/misc.c
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "talloc.h"
|
||||
|
||||
#include "osdep/io.h"
|
||||
#include "osdep/timer.h"
|
||||
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "mpvcore/options.h"
|
||||
#include "mpvcore/m_property.h"
|
||||
#include "mpvcore/mp_common.h"
|
||||
#include "mpvcore/resolve.h"
|
||||
#include "mpvcore/encode.h"
|
||||
#include "mpvcore/playlist.h"
|
||||
#include "mpvcore/input/input.h"
|
||||
|
||||
#include "audio/out/ao.h"
|
||||
#include "demux/demux.h"
|
||||
#include "stream/stream.h"
|
||||
#include "video/out/vo.h"
|
||||
|
||||
#include "mp_core.h"
|
||||
#include "command.h"
|
||||
|
||||
double get_relative_time(struct MPContext *mpctx)
|
||||
{
|
||||
int64_t new_time = mp_time_us();
|
||||
int64_t delta = new_time - mpctx->last_time;
|
||||
mpctx->last_time = new_time;
|
||||
return delta * 0.000001;
|
||||
}
|
||||
|
||||
double rel_time_to_abs(struct MPContext *mpctx, struct m_rel_time t,
|
||||
double fallback_time)
|
||||
{
|
||||
double length = get_time_length(mpctx);
|
||||
switch (t.type) {
|
||||
case REL_TIME_ABSOLUTE:
|
||||
return t.pos;
|
||||
case REL_TIME_NEGATIVE:
|
||||
if (length != 0)
|
||||
return MPMAX(length - t.pos, 0.0);
|
||||
break;
|
||||
case REL_TIME_PERCENT:
|
||||
if (length != 0)
|
||||
return length * (t.pos / 100.0);
|
||||
break;
|
||||
case REL_TIME_CHAPTER:
|
||||
if (chapter_start_time(mpctx, t.pos) >= 0)
|
||||
return chapter_start_time(mpctx, t.pos);
|
||||
break;
|
||||
}
|
||||
return fallback_time;
|
||||
}
|
||||
|
||||
double get_play_end_pts(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
if (opts->play_end.type) {
|
||||
return rel_time_to_abs(mpctx, opts->play_end, MP_NOPTS_VALUE);
|
||||
} else if (opts->play_length.type) {
|
||||
double startpts = get_start_time(mpctx);
|
||||
double start = rel_time_to_abs(mpctx, opts->play_start, startpts);
|
||||
double length = rel_time_to_abs(mpctx, opts->play_length, -1);
|
||||
if (start != -1 && length != -1)
|
||||
return start + length;
|
||||
}
|
||||
return MP_NOPTS_VALUE;
|
||||
}
|
||||
|
||||
// Time used to seek external tracks to.
|
||||
double get_main_demux_pts(struct MPContext *mpctx)
|
||||
{
|
||||
double main_new_pos = MP_NOPTS_VALUE;
|
||||
if (mpctx->demuxer) {
|
||||
for (int n = 0; n < mpctx->demuxer->num_streams; n++) {
|
||||
if (main_new_pos == MP_NOPTS_VALUE)
|
||||
main_new_pos = demux_get_next_pts(mpctx->demuxer->streams[n]);
|
||||
}
|
||||
}
|
||||
return main_new_pos;
|
||||
}
|
||||
|
||||
double get_start_time(struct MPContext *mpctx)
|
||||
{
|
||||
struct demuxer *demuxer = mpctx->demuxer;
|
||||
if (!demuxer)
|
||||
return 0;
|
||||
return demuxer_get_start_time(demuxer);
|
||||
}
|
||||
|
||||
int mp_get_cache_percent(struct MPContext *mpctx)
|
||||
{
|
||||
if (mpctx->stream) {
|
||||
int64_t size = -1;
|
||||
int64_t fill = -1;
|
||||
stream_control(mpctx->stream, STREAM_CTRL_GET_CACHE_SIZE, &size);
|
||||
stream_control(mpctx->stream, STREAM_CTRL_GET_CACHE_FILL, &fill);
|
||||
if (size > 0 && fill >= 0)
|
||||
return fill / (size / 100);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool mp_get_cache_idle(struct MPContext *mpctx)
|
||||
{
|
||||
int idle = 0;
|
||||
if (mpctx->stream)
|
||||
stream_control(mpctx->stream, STREAM_CTRL_GET_CACHE_IDLE, &idle);
|
||||
return idle;
|
||||
}
|
||||
|
||||
void update_window_title(struct MPContext *mpctx, bool force)
|
||||
{
|
||||
if (!mpctx->video_out && !mpctx->ao) {
|
||||
talloc_free(mpctx->last_window_title);
|
||||
mpctx->last_window_title = false;
|
||||
return;
|
||||
}
|
||||
char *title = mp_property_expand_string(mpctx, mpctx->opts->wintitle);
|
||||
if (!mpctx->last_window_title || force ||
|
||||
strcmp(title, mpctx->last_window_title) != 0)
|
||||
{
|
||||
talloc_free(mpctx->last_window_title);
|
||||
mpctx->last_window_title = talloc_steal(mpctx, title);
|
||||
|
||||
if (mpctx->video_out) {
|
||||
mpctx->video_out->window_title = talloc_strdup(mpctx->video_out, title);
|
||||
vo_control(mpctx->video_out, VOCTRL_UPDATE_WINDOW_TITLE, title);
|
||||
}
|
||||
|
||||
if (mpctx->ao) {
|
||||
ao_control(mpctx->ao, AOCONTROL_UPDATE_STREAM_TITLE, title);
|
||||
}
|
||||
} else {
|
||||
talloc_free(title);
|
||||
}
|
||||
}
|
||||
|
||||
void stream_dump(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
char *filename = opts->stream_dump;
|
||||
stream_t *stream = mpctx->stream;
|
||||
assert(stream && filename);
|
||||
|
||||
stream_set_capture_file(stream, filename);
|
||||
|
||||
while (mpctx->stop_play == KEEP_PLAYING && !stream->eof) {
|
||||
if (!opts->quiet && ((stream->pos / (1024 * 1024)) % 2) == 1) {
|
||||
uint64_t pos = stream->pos - stream->start_pos;
|
||||
uint64_t end = stream->end_pos - stream->start_pos;
|
||||
char *line = talloc_asprintf(NULL, "Dumping %lld/%lld...",
|
||||
(long long int)pos, (long long int)end);
|
||||
write_status_line(mpctx, line);
|
||||
talloc_free(line);
|
||||
}
|
||||
stream_fill_buffer(stream);
|
||||
for (;;) {
|
||||
mp_cmd_t *cmd = mp_input_get_cmd(mpctx->input, 0, false);
|
||||
if (!cmd)
|
||||
break;
|
||||
run_command(mpctx, cmd);
|
||||
talloc_free(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void merge_playlist_files(struct playlist *pl)
|
||||
{
|
||||
if (!pl->first)
|
||||
return;
|
||||
char *edl = talloc_strdup(NULL, "edl://");
|
||||
for (struct playlist_entry *e = pl->first; e; e = e->next) {
|
||||
if (e != pl->first)
|
||||
edl = talloc_strdup_append_buffer(edl, ";");
|
||||
// Escape if needed
|
||||
if (e->filename[strcspn(e->filename, "=%,;\n")] ||
|
||||
bstr_strip(bstr0(e->filename)).len != strlen(e->filename))
|
||||
{
|
||||
// %length%
|
||||
edl = talloc_asprintf_append_buffer(edl, "%%%zd%%", strlen(e->filename));
|
||||
}
|
||||
edl = talloc_strdup_append_buffer(edl, e->filename);
|
||||
}
|
||||
playlist_clear(pl);
|
||||
playlist_add_file(pl, edl);
|
||||
talloc_free(edl);
|
||||
}
|
||||
459
player/mp_core.h
Normal file
459
player/mp_core.h
Normal file
@@ -0,0 +1,459 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPLAYER_MP_CORE_H
|
||||
#define MPLAYER_MP_CORE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "mpvcore/mp_common.h"
|
||||
#include "mpvcore/options.h"
|
||||
|
||||
// definitions used internally by the core player code
|
||||
|
||||
#define INITIALIZED_VO 1
|
||||
#define INITIALIZED_AO 2
|
||||
#define INITIALIZED_PLAYBACK 16
|
||||
#define INITIALIZED_LIBASS 32
|
||||
#define INITIALIZED_STREAM 64
|
||||
#define INITIALIZED_DEMUXER 512
|
||||
#define INITIALIZED_ACODEC 1024
|
||||
#define INITIALIZED_VCODEC 2048
|
||||
#define INITIALIZED_SUB 4096
|
||||
#define INITIALIZED_ALL 0xFFFF
|
||||
|
||||
|
||||
enum stop_play_reason {
|
||||
KEEP_PLAYING = 0, // must be 0, numeric values of others do not matter
|
||||
AT_END_OF_FILE, // file has ended, prepare to play next
|
||||
// also returned on unrecoverable playback errors
|
||||
PT_NEXT_ENTRY, // prepare to play next entry in playlist
|
||||
PT_CURRENT_ENTRY, // prepare to play mpctx->playlist->current
|
||||
PT_STOP, // stop playback, clear playlist
|
||||
PT_RESTART, // restart previous file
|
||||
PT_RELOAD_DEMUXER, // restart playback, but keep stream open
|
||||
PT_QUIT, // stop playback, quit player
|
||||
};
|
||||
|
||||
enum exit_reason {
|
||||
EXIT_NONE,
|
||||
EXIT_QUIT,
|
||||
EXIT_PLAYED,
|
||||
EXIT_ERROR,
|
||||
EXIT_NOTPLAYED,
|
||||
EXIT_SOMENOTPLAYED
|
||||
};
|
||||
|
||||
struct timeline_part {
|
||||
double start;
|
||||
double source_start;
|
||||
struct demuxer *source;
|
||||
};
|
||||
|
||||
struct chapter {
|
||||
double start;
|
||||
char *name;
|
||||
};
|
||||
|
||||
enum mp_osd_seek_info {
|
||||
OSD_SEEK_INFO_BAR = 1,
|
||||
OSD_SEEK_INFO_TEXT = 2,
|
||||
OSD_SEEK_INFO_CHAPTER_TEXT = 4,
|
||||
OSD_SEEK_INFO_EDITION = 8,
|
||||
};
|
||||
|
||||
|
||||
enum {
|
||||
OSD_MSG_TEXT = 1,
|
||||
OSD_MSG_SUB_DELAY,
|
||||
OSD_MSG_SPEED,
|
||||
OSD_MSG_OSD_STATUS,
|
||||
OSD_MSG_BAR,
|
||||
OSD_MSG_PAUSE,
|
||||
OSD_MSG_RADIO_CHANNEL,
|
||||
OSD_MSG_TV_CHANNEL,
|
||||
|
||||
// Base id for messages generated from the commmand to property bridge.
|
||||
OSD_MSG_PROPERTY = 0x100,
|
||||
OSD_MSG_SUB_BASE = 0x1000,
|
||||
|
||||
// other constants
|
||||
MAX_OSD_LEVEL = 3,
|
||||
MAX_TERM_OSD_LEVEL = 1,
|
||||
OSD_LEVEL_INVISIBLE = 4,
|
||||
OSD_BAR_SEEK = 256,
|
||||
};
|
||||
|
||||
enum seek_type {
|
||||
MPSEEK_NONE = 0,
|
||||
MPSEEK_RELATIVE,
|
||||
MPSEEK_ABSOLUTE,
|
||||
MPSEEK_FACTOR,
|
||||
};
|
||||
|
||||
struct track {
|
||||
enum stream_type type;
|
||||
// The type specific ID, also called aid (audio), sid (subs), vid (video).
|
||||
// For UI purposes only; this ID doesn't have anything to do with any
|
||||
// IDs coming from demuxers or container files.
|
||||
int user_tid;
|
||||
|
||||
// Same as stream->demuxer_id. -1 if not set.
|
||||
int demuxer_id;
|
||||
|
||||
char *title;
|
||||
bool default_track;
|
||||
bool attached_picture;
|
||||
char *lang;
|
||||
|
||||
// If this track is from an external file (e.g. subtitle file).
|
||||
bool is_external;
|
||||
char *external_filename;
|
||||
bool auto_loaded;
|
||||
|
||||
// If the track's stream changes with the timeline (ordered chapters).
|
||||
bool under_timeline;
|
||||
|
||||
// Value can change if under_timeline==true.
|
||||
struct demuxer *demuxer;
|
||||
// Invariant: !stream || stream->demuxer == demuxer
|
||||
struct sh_stream *stream;
|
||||
|
||||
// For external subtitles, which are read fully on init. Do not attempt
|
||||
// to read packets from them.
|
||||
bool preloaded;
|
||||
};
|
||||
|
||||
enum {
|
||||
MAX_NUM_VO_PTS = 100,
|
||||
};
|
||||
|
||||
typedef struct MPContext {
|
||||
struct mpv_global *global;
|
||||
struct MPOpts *opts;
|
||||
struct mp_log *log;
|
||||
struct m_config *mconfig;
|
||||
struct input_ctx *input;
|
||||
struct osd_state *osd;
|
||||
struct mp_osd_msg *osd_msg_stack;
|
||||
char *terminal_osd_text;
|
||||
char *last_window_title;
|
||||
|
||||
int add_osd_seek_info; // bitfield of enum mp_osd_seek_info
|
||||
double osd_visible; // for the osd bar only
|
||||
int osd_function;
|
||||
double osd_function_visible;
|
||||
double osd_last_update;
|
||||
|
||||
struct playlist *playlist;
|
||||
char *filename; // currently playing file
|
||||
struct mp_resolve_result *resolve_result;
|
||||
enum stop_play_reason stop_play;
|
||||
unsigned int initialized_flags; // which subsystems have been initialized
|
||||
|
||||
// Return code to use with PT_QUIT
|
||||
enum exit_reason quit_player_rc;
|
||||
int quit_custom_rc;
|
||||
bool has_quit_custom_rc;
|
||||
bool error_playing;
|
||||
|
||||
int64_t shown_vframes, shown_aframes;
|
||||
|
||||
struct demuxer **sources;
|
||||
int num_sources;
|
||||
|
||||
struct timeline_part *timeline;
|
||||
int num_timeline_parts;
|
||||
int timeline_part;
|
||||
// NOTE: even if num_chapters==0, chapters being not NULL signifies presence
|
||||
// of chapter metadata
|
||||
struct chapter *chapters;
|
||||
int num_chapters;
|
||||
double video_offset;
|
||||
|
||||
struct stream *stream;
|
||||
struct demuxer *demuxer;
|
||||
|
||||
struct track **tracks;
|
||||
int num_tracks;
|
||||
|
||||
char *track_layout_hash;
|
||||
|
||||
// Selected tracks. NULL if no track selected.
|
||||
struct track *current_track[STREAM_TYPE_COUNT];
|
||||
|
||||
struct sh_stream *sh[STREAM_TYPE_COUNT];
|
||||
|
||||
struct dec_video *d_video;
|
||||
struct dec_audio *d_audio;
|
||||
struct dec_sub *d_sub;
|
||||
|
||||
// Uses: accessing metadata (consider ordered chapters case, where the main
|
||||
// demuxer defines metadata), or special purpose demuxers like TV.
|
||||
struct demuxer *master_demuxer;
|
||||
|
||||
struct mixer *mixer;
|
||||
struct ao *ao;
|
||||
struct vo *video_out;
|
||||
|
||||
/* We're starting playback from scratch or after a seek. Show first
|
||||
* video frame immediately and reinitialize sync. */
|
||||
bool restart_playback;
|
||||
/* Set if audio should be timed to start with video frame after seeking,
|
||||
* not set when e.g. playing cover art */
|
||||
bool sync_audio_to_video;
|
||||
/* After playback restart (above) or audio stream change, adjust audio
|
||||
* stream by cutting samples or adding silence at the beginning to make
|
||||
* audio playback position match video position. */
|
||||
bool syncing_audio;
|
||||
bool hrseek_active;
|
||||
bool hrseek_framedrop;
|
||||
double hrseek_pts;
|
||||
// AV sync: the next frame should be shown when the audio out has this
|
||||
// much (in seconds) buffered data left. Increased when more data is
|
||||
// written to the ao, decreased when moving to the next frame.
|
||||
// In the audio-only case used as a timer since the last seek
|
||||
// by the audio CPU usage meter.
|
||||
double delay;
|
||||
// AV sync: time until next frame should be shown
|
||||
double time_frame;
|
||||
// How long the last vo flip() call took. Used to adjust timing with
|
||||
// the goal of making flip() calls finish (rather than start) at the
|
||||
// specified time.
|
||||
double last_vo_flip_duration;
|
||||
// Display duration (as "intended") of the last flipped frame.
|
||||
double last_frame_duration;
|
||||
// Set to true some time after a new frame has been shown, and it turns out
|
||||
// that this frame was the last one before video ends.
|
||||
bool playing_last_frame;
|
||||
// How much video timing has been changed to make it match the audio
|
||||
// timeline. Used for status line information only.
|
||||
double total_avsync_change;
|
||||
// Total number of dropped frames that were "approved" to be dropped.
|
||||
// Actual dropping depends on --framedrop and decoder internals.
|
||||
int drop_frame_cnt;
|
||||
// Number of frames dropped in a row.
|
||||
int dropped_frames;
|
||||
// A-V sync difference when last frame was displayed. Kept to display
|
||||
// the same value if the status line is updated at a time where no new
|
||||
// video frame is shown.
|
||||
double last_av_difference;
|
||||
/* Timestamp of the latest image that was queued on the VO, but not yet
|
||||
* to be flipped. */
|
||||
double video_next_pts;
|
||||
/* timestamp of video frame currently visible on screen
|
||||
* (or at least queued to be flipped by VO) */
|
||||
double video_pts;
|
||||
double last_seek_pts;
|
||||
// As video_pts, but is not reset when seeking away. (For the very short
|
||||
// period of time until a new frame is decoded and shown.)
|
||||
double last_vo_pts;
|
||||
// Video PTS, or audio PTS if video has ended.
|
||||
double playback_pts;
|
||||
|
||||
// History of video frames timestamps that were queued in the VO
|
||||
// This includes even skipped frames during hr-seek
|
||||
double vo_pts_history_pts[MAX_NUM_VO_PTS];
|
||||
// Whether the PTS at vo_pts_history[n] is after a seek reset
|
||||
uint64_t vo_pts_history_seek[MAX_NUM_VO_PTS];
|
||||
uint64_t vo_pts_history_seek_ts;
|
||||
uint64_t backstep_start_seek_ts;
|
||||
bool backstep_active;
|
||||
|
||||
double audio_delay;
|
||||
|
||||
double last_heartbeat;
|
||||
double last_metadata_update;
|
||||
|
||||
double mouse_timer;
|
||||
unsigned int mouse_event_ts;
|
||||
bool mouse_cursor_visible;
|
||||
|
||||
// used to prevent hanging in some error cases
|
||||
double start_timestamp;
|
||||
|
||||
// Timestamp from the last time some timing functions read the
|
||||
// current time, in microseconds.
|
||||
// Used to turn a new time value to a delta from last time.
|
||||
int64_t last_time;
|
||||
|
||||
// Used to communicate the parameters of a seek between parts
|
||||
struct seek_params {
|
||||
enum seek_type type;
|
||||
double amount;
|
||||
int exact; // -1 = disable, 0 = default, 1 = enable
|
||||
// currently not set by commands, only used internally by seek()
|
||||
int direction; // -1 = backward, 0 = default, 1 = forward
|
||||
} seek;
|
||||
|
||||
/* Heuristic for relative chapter seeks: keep track which chapter
|
||||
* the user wanted to go to, even if we aren't exactly within the
|
||||
* boundaries of that chapter due to an inaccurate seek. */
|
||||
int last_chapter_seek;
|
||||
double last_chapter_pts;
|
||||
|
||||
/* Subtitle renderer. This is separate, because we want to keep fonts
|
||||
* loaded across ordered chapters, instead of reloading and rescanning
|
||||
* them on each transition. (Both of these objects contain this state.)
|
||||
*/
|
||||
struct ass_renderer *ass_renderer;
|
||||
struct ass_library *ass_library;
|
||||
|
||||
int last_dvb_step;
|
||||
|
||||
bool paused;
|
||||
// step this many frames, then pause
|
||||
int step_frames;
|
||||
// Counted down each frame, stop playback if 0 is reached. (-1 = disable)
|
||||
int max_frames;
|
||||
bool playing_msg_shown;
|
||||
|
||||
bool paused_for_cache;
|
||||
|
||||
// Set after showing warning about decoding being too slow for realtime
|
||||
// playback rate. Used to avoid showing it multiple times.
|
||||
bool drop_message_shown;
|
||||
|
||||
struct screenshot_ctx *screenshot_ctx;
|
||||
struct command_ctx *command_ctx;
|
||||
struct encode_lavc_context *encode_lavc_ctx;
|
||||
struct lua_ctx *lua_ctx;
|
||||
struct mp_nav_state *nav_state;
|
||||
} MPContext;
|
||||
|
||||
// audio.c
|
||||
void reinit_audio_chain(struct MPContext *mpctx);
|
||||
int reinit_audio_filters(struct MPContext *mpctx);
|
||||
double playing_audio_pts(struct MPContext *mpctx);
|
||||
int fill_audio_out_buffers(struct MPContext *mpctx, double endpts);
|
||||
double written_audio_pts(struct MPContext *mpctx);
|
||||
void clear_audio_output_buffers(struct MPContext *mpctx);
|
||||
void clear_audio_decode_buffers(struct MPContext *mpctx);
|
||||
|
||||
// configfiles.c
|
||||
bool mp_parse_cfgfiles(struct MPContext *mpctx);
|
||||
char *mp_get_playback_resume_config_filename(const char *fname,
|
||||
struct MPOpts *opts);
|
||||
void mp_load_per_protocol_config(struct m_config *conf, const char * const file);
|
||||
void mp_load_per_extension_config(struct m_config *conf, const char * const file);
|
||||
void mp_load_per_output_config(struct m_config *conf, char *cfg, char *out);
|
||||
void mp_load_per_file_config(struct m_config *conf, const char * const file,
|
||||
bool search_file_dir);
|
||||
void mp_load_playback_resume(struct m_config *conf, const char *file);
|
||||
void mp_write_watch_later_conf(struct MPContext *mpctx);
|
||||
struct playlist_entry *mp_resume_playlist(struct playlist *playlist,
|
||||
struct MPOpts *opts);
|
||||
|
||||
// dvdnav.c
|
||||
void mp_nav_init(struct MPContext *mpctx);
|
||||
void mp_nav_reset(struct MPContext *mpctx);
|
||||
void mp_nav_destroy(struct MPContext *mpctx);
|
||||
void mp_nav_user_input(struct MPContext *mpctx, char *command);
|
||||
void mp_handle_nav(struct MPContext *mpctx);
|
||||
|
||||
// loadfile.c
|
||||
void uninit_player(struct MPContext *mpctx, unsigned int mask);
|
||||
struct track *mp_add_subtitles(struct MPContext *mpctx, char *filename);
|
||||
void mp_switch_track(struct MPContext *mpctx, enum stream_type type,
|
||||
struct track *track);
|
||||
struct track *mp_track_by_tid(struct MPContext *mpctx, enum stream_type type,
|
||||
int tid);
|
||||
bool timeline_set_part(struct MPContext *mpctx, int i, bool force);
|
||||
double timeline_set_from_time(struct MPContext *mpctx, double pts, bool *need_reset);
|
||||
struct sh_stream *init_demux_stream(struct MPContext *mpctx,
|
||||
enum stream_type type);
|
||||
void cleanup_demux_stream(struct MPContext *mpctx, enum stream_type type);
|
||||
void add_demuxer_tracks(struct MPContext *mpctx, struct demuxer *demuxer);
|
||||
bool mp_remove_track(struct MPContext *mpctx, struct track *track);
|
||||
struct playlist_entry *mp_next_file(struct MPContext *mpctx, int direction,
|
||||
bool force);
|
||||
void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e);
|
||||
void mp_play_files(struct MPContext *mpctx);
|
||||
|
||||
// main.c
|
||||
void mp_print_version(int always);
|
||||
|
||||
// misc.c
|
||||
double get_start_time(struct MPContext *mpctx);
|
||||
double get_main_demux_pts(struct MPContext *mpctx);
|
||||
double rel_time_to_abs(struct MPContext *mpctx, struct m_rel_time t,
|
||||
double fallback_time);
|
||||
double get_play_end_pts(struct MPContext *mpctx);
|
||||
double get_relative_time(struct MPContext *mpctx);
|
||||
void merge_playlist_files(struct playlist *pl);
|
||||
int mp_get_cache_percent(struct MPContext *mpctx);
|
||||
bool mp_get_cache_idle(struct MPContext *mpctx);
|
||||
void update_window_title(struct MPContext *mpctx, bool force);
|
||||
void stream_dump(struct MPContext *mpctx);
|
||||
|
||||
// osd.c
|
||||
void write_status_line(struct MPContext *mpctx, const char *line);
|
||||
void print_status(struct MPContext *mpctx);
|
||||
void set_osd_bar(struct MPContext *mpctx, int type, const char* name,
|
||||
double min, double max, double val);
|
||||
void set_osd_msg(struct MPContext *mpctx, int id, int level, int time,
|
||||
const char* fmt, ...) PRINTF_ATTRIBUTE(5,6);
|
||||
void rm_osd_msg(struct MPContext *mpctx, int id);
|
||||
void set_osd_function(struct MPContext *mpctx, int osd_function);
|
||||
void set_osd_subtitle(struct MPContext *mpctx, const char *text);
|
||||
|
||||
// playloop.c
|
||||
void pause_player(struct MPContext *mpctx);
|
||||
void unpause_player(struct MPContext *mpctx);
|
||||
void add_step_frame(struct MPContext *mpctx, int dir);
|
||||
void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount,
|
||||
int exact);
|
||||
bool mp_seek_chapter(struct MPContext *mpctx, int chapter);
|
||||
double get_time_length(struct MPContext *mpctx);
|
||||
double get_current_time(struct MPContext *mpctx);
|
||||
int get_percent_pos(struct MPContext *mpctx);
|
||||
double get_current_pos_ratio(struct MPContext *mpctx, bool use_range);
|
||||
int get_current_chapter(struct MPContext *mpctx);
|
||||
char *chapter_display_name(struct MPContext *mpctx, int chapter);
|
||||
char *chapter_name(struct MPContext *mpctx, int chapter);
|
||||
double chapter_start_time(struct MPContext *mpctx, int chapter);
|
||||
int get_chapter_count(struct MPContext *mpctx);
|
||||
void execute_queued_seek(struct MPContext *mpctx);
|
||||
void run_playloop(struct MPContext *mpctx);
|
||||
void idle_loop(struct MPContext *mpctx);
|
||||
void handle_force_window(struct MPContext *mpctx, bool reconfig);
|
||||
void add_frame_pts(struct MPContext *mpctx, double pts);
|
||||
|
||||
// sub.c
|
||||
void reset_subtitles(struct MPContext *mpctx);
|
||||
void uninit_subs(struct demuxer *demuxer);
|
||||
void reinit_subs(struct MPContext *mpctx);
|
||||
void update_osd_msg(struct MPContext *mpctx);
|
||||
void update_subtitles(struct MPContext *mpctx);
|
||||
|
||||
// timeline/tl_matroska.c
|
||||
void build_ordered_chapter_timeline(struct MPContext *mpctx);
|
||||
// timeline/tl_mpv_edl.c
|
||||
void build_mpv_edl_timeline(struct MPContext *mpctx);
|
||||
// timeline/tl_cue.c
|
||||
void build_cue_timeline(struct MPContext *mpctx);
|
||||
|
||||
// video.c
|
||||
int reinit_video_chain(struct MPContext *mpctx);
|
||||
int reinit_video_filters(struct MPContext *mpctx);
|
||||
double update_video(struct MPContext *mpctx, double endpts);
|
||||
void mp_force_video_refresh(struct MPContext *mpctx);
|
||||
void update_fps(struct MPContext *mpctx);
|
||||
void video_execute_format_change(struct MPContext *mpctx);
|
||||
|
||||
#endif /* MPLAYER_MP_CORE_H */
|
||||
683
player/mp_lua.c
Normal file
683
player/mp_lua.c
Normal file
@@ -0,0 +1,683 @@
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <lua.h>
|
||||
#include <lualib.h>
|
||||
#include <lauxlib.h>
|
||||
|
||||
#include "talloc.h"
|
||||
|
||||
#include "mpvcore/mp_common.h"
|
||||
#include "mpvcore/m_property.h"
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "mpvcore/m_option.h"
|
||||
#include "mpvcore/input/input.h"
|
||||
#include "mpvcore/path.h"
|
||||
#include "mpvcore/bstr.h"
|
||||
#include "osdep/timer.h"
|
||||
#include "sub/osd.h"
|
||||
#include "mp_core.h"
|
||||
#include "command.h"
|
||||
#include "mp_lua.h"
|
||||
|
||||
// List of builtin modules and their contents as strings.
|
||||
// All these are generated from player/lua/*.lua
|
||||
static const char *builtin_lua_scripts[][2] = {
|
||||
{"mp.defaults",
|
||||
# include "player/lua/defaults.inc"
|
||||
},
|
||||
{"mp.assdraw",
|
||||
# include "player/lua/assdraw.inc"
|
||||
},
|
||||
{"@osc",
|
||||
# include "player/lua/osc.inc"
|
||||
},
|
||||
{0}
|
||||
};
|
||||
|
||||
// Represents a loaded script. Each has its own Lua state.
|
||||
struct script_ctx {
|
||||
const char *name;
|
||||
lua_State *state;
|
||||
struct mp_log *log;
|
||||
struct MPContext *mpctx;
|
||||
};
|
||||
|
||||
struct lua_ctx {
|
||||
struct script_ctx **scripts;
|
||||
int num_scripts;
|
||||
};
|
||||
|
||||
static struct script_ctx *find_script(struct lua_ctx *lctx, const char *name)
|
||||
{
|
||||
for (int n = 0; n < lctx->num_scripts; n++) {
|
||||
if (strcmp(lctx->scripts[n]->name, name) == 0)
|
||||
return lctx->scripts[n];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct script_ctx *get_ctx(lua_State *L)
|
||||
{
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "ctx");
|
||||
struct script_ctx *ctx = lua_touserdata(L, -1);
|
||||
lua_pop(L, 1);
|
||||
assert(ctx);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static struct MPContext *get_mpctx(lua_State *L)
|
||||
{
|
||||
return get_ctx(L)->mpctx;
|
||||
}
|
||||
|
||||
static int wrap_cpcall(lua_State *L)
|
||||
{
|
||||
lua_CFunction fn = lua_touserdata(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return fn(L);
|
||||
}
|
||||
|
||||
// Call the given function fn under a Lua error handler (similar to lua_cpcall).
|
||||
// Pass the given number of args from the Lua stack to fn.
|
||||
// Returns 0 (and empty stack) on success.
|
||||
// Returns LUA_ERR[RUN|MEM|ERR] otherwise, with the error value on the stack.
|
||||
static int mp_cpcall(lua_State *L, lua_CFunction fn, int args)
|
||||
{
|
||||
// Don't use lua_pushcfunction() - it allocates memory on Lua 5.1.
|
||||
// Instead, emulate C closures by making wrap_cpcall call fn.
|
||||
lua_pushlightuserdata(L, fn); // args... fn
|
||||
// Will always succeed if mp_lua_init() set it up correctly.
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "wrap_cpcall"); // args... fn wrap_cpcall
|
||||
lua_insert(L, -(args + 2)); // wrap_cpcall args... fn
|
||||
return lua_pcall(L, args + 1, 0, 0);
|
||||
}
|
||||
|
||||
static void report_error(lua_State *L)
|
||||
{
|
||||
const char *err = lua_tostring(L, -1);
|
||||
mp_msg(MSGT_CPLAYER, MSGL_WARN, "[lua] Error: %s\n",
|
||||
err ? err : "[unknown]");
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
static void add_functions(struct script_ctx *ctx);
|
||||
|
||||
static char *script_name_from_filename(void *talloc_ctx, struct lua_ctx *lctx,
|
||||
const char *fname)
|
||||
{
|
||||
fname = mp_basename(fname);
|
||||
if (fname[0] == '@')
|
||||
fname += 1;
|
||||
char *name = talloc_strdup(talloc_ctx, fname);
|
||||
// Drop .lua extension
|
||||
char *dot = strrchr(name, '.');
|
||||
if (dot)
|
||||
*dot = '\0';
|
||||
// Turn it into a safe identifier - this is used with e.g. dispatching
|
||||
// input via: "send scriptname ..."
|
||||
for (int n = 0; name[n]; n++) {
|
||||
char c = name[n];
|
||||
if (!(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') &&
|
||||
!(c >= '0' && c <= '9'))
|
||||
name[n] = '_';
|
||||
}
|
||||
// Make unique (stupid but simple)
|
||||
while (find_script(lctx, name))
|
||||
name = talloc_strdup_append(name, "_");
|
||||
return name;
|
||||
}
|
||||
|
||||
static int load_file(struct script_ctx *ctx, const char *fname)
|
||||
{
|
||||
int r = 0;
|
||||
lua_State *L = ctx->state;
|
||||
if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0)) {
|
||||
report_error(L);
|
||||
r = -1;
|
||||
}
|
||||
assert(lua_gettop(L) == 0);
|
||||
return r;
|
||||
}
|
||||
|
||||
static int load_builtin(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
for (int n = 0; builtin_lua_scripts[n][0]; n++) {
|
||||
if (strcmp(name, builtin_lua_scripts[n][0]) == 0) {
|
||||
if (luaL_loadstring(L, builtin_lua_scripts[n][1]))
|
||||
lua_error(L);
|
||||
lua_call(L, 0, 1);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Execute "require " .. name
|
||||
static bool require(lua_State *L, const char *name)
|
||||
{
|
||||
char buf[80];
|
||||
// Lazy, but better than calling the "require" function manually
|
||||
snprintf(buf, sizeof(buf), "require '%s'", name);
|
||||
if (luaL_loadstring(L, buf) || lua_pcall(L, 0, 0, 0)) {
|
||||
report_error(L);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void mp_lua_load_script(struct MPContext *mpctx, const char *fname)
|
||||
{
|
||||
struct lua_ctx *lctx = mpctx->lua_ctx;
|
||||
struct script_ctx *ctx = talloc_ptrtype(NULL, ctx);
|
||||
*ctx = (struct script_ctx) {
|
||||
.mpctx = mpctx,
|
||||
.name = script_name_from_filename(ctx, lctx, fname),
|
||||
};
|
||||
char *log_name = talloc_asprintf(ctx, "lua/%s", ctx->name);
|
||||
ctx->log = mp_log_new(ctx, mpctx->log, log_name);
|
||||
|
||||
lua_State *L = ctx->state = luaL_newstate();
|
||||
if (!L)
|
||||
goto error_out;
|
||||
|
||||
// used by get_ctx()
|
||||
lua_pushlightuserdata(L, ctx); // ctx
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "ctx"); // -
|
||||
|
||||
lua_pushcfunction(L, wrap_cpcall); // closure
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "wrap_cpcall"); // -
|
||||
|
||||
luaL_openlibs(L);
|
||||
|
||||
lua_newtable(L); // mp
|
||||
lua_pushvalue(L, -1); // mp mp
|
||||
lua_setglobal(L, "mp"); // mp
|
||||
|
||||
add_functions(ctx); // mp
|
||||
|
||||
lua_pushstring(L, ctx->name); // mp name
|
||||
lua_setfield(L, -2, "script_name"); // mp
|
||||
|
||||
lua_pop(L, 1); // -
|
||||
|
||||
// Add a preloader for each builtin Lua module
|
||||
lua_getglobal(L, "package"); // package
|
||||
assert(lua_type(L, -1) == LUA_TTABLE);
|
||||
lua_getfield(L, -1, "preload"); // package preload
|
||||
assert(lua_type(L, -1) == LUA_TTABLE);
|
||||
for (int n = 0; builtin_lua_scripts[n][0]; n++) {
|
||||
lua_pushcfunction(L, load_builtin); // package preload load_builtin
|
||||
lua_setfield(L, -2, builtin_lua_scripts[n][0]);
|
||||
}
|
||||
lua_pop(L, 2); // -
|
||||
|
||||
assert(lua_gettop(L) == 0);
|
||||
|
||||
if (!require(L, "mp.defaults")) {
|
||||
report_error(L);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
assert(lua_gettop(L) == 0);
|
||||
|
||||
if (fname[0] == '@') {
|
||||
if (!require(L, fname))
|
||||
goto error_out;
|
||||
} else {
|
||||
if (load_file(ctx, fname) < 0)
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
MP_TARRAY_APPEND(lctx, lctx->scripts, lctx->num_scripts, ctx);
|
||||
return;
|
||||
|
||||
error_out:
|
||||
if (ctx->state)
|
||||
lua_close(ctx->state);
|
||||
talloc_free(ctx);
|
||||
}
|
||||
|
||||
static void kill_script(struct script_ctx *ctx)
|
||||
{
|
||||
if (!ctx)
|
||||
return;
|
||||
struct lua_ctx *lctx = ctx->mpctx->lua_ctx;
|
||||
lua_close(ctx->state);
|
||||
for (int n = 0; n < lctx->num_scripts; n++) {
|
||||
if (lctx->scripts[n] == ctx) {
|
||||
MP_TARRAY_REMOVE_AT(lctx->scripts, lctx->num_scripts, n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
talloc_free(ctx);
|
||||
}
|
||||
|
||||
static const char *log_level[] = {
|
||||
[MSGL_FATAL] = "fatal",
|
||||
[MSGL_ERR] = "error",
|
||||
[MSGL_WARN] = "warn",
|
||||
[MSGL_INFO] = "info",
|
||||
[MSGL_V] = "verbose",
|
||||
[MSGL_DBG2] = "debug",
|
||||
};
|
||||
|
||||
static int script_log(lua_State *L)
|
||||
{
|
||||
struct script_ctx *ctx = get_ctx(L);
|
||||
|
||||
const char *level = luaL_checkstring(L, 1);
|
||||
int msgl = -1;
|
||||
for (int n = 0; n < MP_ARRAY_SIZE(log_level); n++) {
|
||||
if (log_level[n] && strcasecmp(log_level[n], level) == 0) {
|
||||
msgl = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (msgl < 0)
|
||||
luaL_error(L, "Invalid log level '%s'", level);
|
||||
|
||||
int last = lua_gettop(L);
|
||||
lua_getglobal(L, "tostring"); // args... tostring
|
||||
for (int i = 2; i <= last; i++) {
|
||||
lua_pushvalue(L, -1); // args... tostring tostring
|
||||
lua_pushvalue(L, i); // args... tostring tostring args[i]
|
||||
lua_call(L, 1, 1); // args... tostring str
|
||||
const char *s = lua_tostring(L, -1);
|
||||
if (s == NULL)
|
||||
return luaL_error(L, "Invalid argument");
|
||||
mp_msg_log(ctx->log, msgl, "%s%s", s, i > 0 ? " " : "");
|
||||
lua_pop(L, 1); // args... tostring
|
||||
}
|
||||
mp_msg_log(ctx->log, msgl, "\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int script_find_config_file(lua_State *L)
|
||||
{
|
||||
const char *s = luaL_checkstring(L, 1);
|
||||
char *path = mp_find_user_config_file(s);
|
||||
if (path) {
|
||||
lua_pushstring(L, path);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
talloc_free(path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int run_event(lua_State *L)
|
||||
{
|
||||
lua_getglobal(L, "mp_event"); // name arg mp_event
|
||||
if (lua_isnil(L, -1))
|
||||
return 0;
|
||||
lua_insert(L, -3); // mp_event name arg
|
||||
lua_call(L, 2, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mp_lua_event(struct MPContext *mpctx, const char *name, const char *arg)
|
||||
{
|
||||
// There is no proper subscription mechanism yet, so all scripts get it.
|
||||
struct lua_ctx *lctx = mpctx->lua_ctx;
|
||||
for (int n = 0; n < lctx->num_scripts; n++) {
|
||||
struct script_ctx *ctx = lctx->scripts[n];
|
||||
lua_State *L = ctx->state;
|
||||
lua_pushstring(L, name);
|
||||
if (arg) {
|
||||
lua_pushstring(L, arg);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
if (mp_cpcall(L, run_event, 2) != 0)
|
||||
report_error(L);
|
||||
}
|
||||
}
|
||||
|
||||
static int run_script_dispatch(lua_State *L)
|
||||
{
|
||||
int id = lua_tointeger(L, 1);
|
||||
const char *event = lua_tostring(L, 2);
|
||||
lua_getglobal(L, "mp_script_dispatch");
|
||||
if (lua_isnil(L, -1))
|
||||
return 0;
|
||||
lua_pushinteger(L, id);
|
||||
lua_pushstring(L, event);
|
||||
lua_call(L, 2, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mp_lua_script_dispatch(struct MPContext *mpctx, char *script_name,
|
||||
int id, char *event)
|
||||
{
|
||||
struct script_ctx *ctx = find_script(mpctx->lua_ctx, script_name);
|
||||
if (!ctx) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_V,
|
||||
"Can't find script '%s' when handling input.\n", script_name);
|
||||
return;
|
||||
}
|
||||
lua_State *L = ctx->state;
|
||||
lua_pushinteger(L, id);
|
||||
lua_pushstring(L, event);
|
||||
if (mp_cpcall(L, run_script_dispatch, 2) != 0)
|
||||
report_error(L);
|
||||
}
|
||||
|
||||
static int script_send_command(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
const char *s = luaL_checkstring(L, 1);
|
||||
|
||||
mp_cmd_t *cmd = mp_input_parse_cmd(mpctx->input, bstr0((char*)s), "<lua>");
|
||||
if (!cmd)
|
||||
luaL_error(L, "error parsing command");
|
||||
mp_input_queue_cmd(mpctx->input, cmd);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int script_property_list(lua_State *L)
|
||||
{
|
||||
const struct m_option *props = mp_get_property_list();
|
||||
lua_newtable(L);
|
||||
for (int i = 0; props[i].name; i++) {
|
||||
lua_pushinteger(L, i + 1);
|
||||
lua_pushstring(L, props[i].name);
|
||||
lua_settable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int script_property_string(lua_State *L)
|
||||
{
|
||||
const struct m_option *props = mp_get_property_list();
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
int type = lua_tointeger(L, lua_upvalueindex(1))
|
||||
? M_PROPERTY_PRINT : M_PROPERTY_GET_STRING;
|
||||
|
||||
char *result = NULL;
|
||||
if (m_property_do(props, name, type, &result, mpctx) >= 0 && result) {
|
||||
lua_pushstring(L, result);
|
||||
talloc_free(result);
|
||||
return 1;
|
||||
}
|
||||
if (type == M_PROPERTY_PRINT) {
|
||||
lua_pushstring(L, "");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int script_set_osd_ass(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
int res_x = luaL_checkinteger(L, 1);
|
||||
int res_y = luaL_checkinteger(L, 2);
|
||||
const char *text = luaL_checkstring(L, 3);
|
||||
if (!mpctx->osd->external ||
|
||||
strcmp(mpctx->osd->external, text) != 0 ||
|
||||
mpctx->osd->external_res_x != res_x ||
|
||||
mpctx->osd->external_res_y != res_y)
|
||||
{
|
||||
talloc_free(mpctx->osd->external);
|
||||
mpctx->osd->external = talloc_strdup(mpctx->osd, text);
|
||||
mpctx->osd->external_res_x = res_x;
|
||||
mpctx->osd->external_res_y = res_y;
|
||||
osd_changed(mpctx->osd, OSDTYPE_EXTERNAL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int script_get_osd_resolution(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
int w, h;
|
||||
osd_object_get_resolution(mpctx->osd, mpctx->osd->objs[OSDTYPE_EXTERNAL],
|
||||
&w, &h);
|
||||
lua_pushnumber(L, w);
|
||||
lua_pushnumber(L, h);
|
||||
return 2;
|
||||
}
|
||||
|
||||
static int script_get_screen_size(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
struct osd_object *obj = mpctx->osd->objs[OSDTYPE_EXTERNAL];
|
||||
double aspect = 1.0 * obj->vo_res.w / MPMAX(obj->vo_res.h, 1) /
|
||||
obj->vo_res.display_par;
|
||||
lua_pushnumber(L, obj->vo_res.w);
|
||||
lua_pushnumber(L, obj->vo_res.h);
|
||||
lua_pushnumber(L, aspect);
|
||||
return 3;
|
||||
}
|
||||
|
||||
static int script_get_mouse_pos(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
int px, py;
|
||||
mp_input_get_mouse_pos(mpctx->input, &px, &py);
|
||||
double sw, sh;
|
||||
osd_object_get_scale_factor(mpctx->osd, mpctx->osd->objs[OSDTYPE_EXTERNAL],
|
||||
&sw, &sh);
|
||||
lua_pushnumber(L, px * sw);
|
||||
lua_pushnumber(L, py * sh);
|
||||
return 2;
|
||||
}
|
||||
|
||||
static int script_get_timer(lua_State *L)
|
||||
{
|
||||
lua_pushnumber(L, mp_time_sec());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int script_get_chapter_list(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
lua_newtable(L); // list
|
||||
int num = get_chapter_count(mpctx);
|
||||
for (int n = 0; n < num; n++) {
|
||||
double time = chapter_start_time(mpctx, n);
|
||||
char *name = chapter_display_name(mpctx, n);
|
||||
lua_newtable(L); // list ch
|
||||
lua_pushnumber(L, time); // list ch time
|
||||
lua_setfield(L, -2, "time"); // list ch
|
||||
lua_pushstring(L, name); // list ch name
|
||||
lua_setfield(L, -2, "name"); // list ch
|
||||
lua_pushinteger(L, n + 1); // list ch n1
|
||||
lua_insert(L, -2); // list n1 ch
|
||||
lua_settable(L, -3); // list
|
||||
talloc_free(name);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const char *stream_type(enum stream_type t)
|
||||
{
|
||||
switch (t) {
|
||||
case STREAM_VIDEO: return "video";
|
||||
case STREAM_AUDIO: return "audio";
|
||||
case STREAM_SUB: return "sub";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static int script_get_track_list(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
lua_newtable(L); // list
|
||||
for (int n = 0; n < mpctx->num_tracks; n++) {
|
||||
struct track *track = mpctx->tracks[n];
|
||||
lua_newtable(L); // list track
|
||||
|
||||
lua_pushstring(L, stream_type(track->type));
|
||||
lua_setfield(L, -2, "type");
|
||||
lua_pushinteger(L, track->user_tid);
|
||||
lua_setfield(L, -2, "id");
|
||||
lua_pushboolean(L, track->default_track);
|
||||
lua_setfield(L, -2, "default");
|
||||
lua_pushboolean(L, track->attached_picture);
|
||||
lua_setfield(L, -2, "attached_picture");
|
||||
if (track->lang) {
|
||||
lua_pushstring(L, track->lang);
|
||||
lua_setfield(L, -2, "language");
|
||||
}
|
||||
if (track->title) {
|
||||
lua_pushstring(L, track->title);
|
||||
lua_setfield(L, -2, "title");
|
||||
}
|
||||
lua_pushboolean(L, track->is_external);
|
||||
lua_setfield(L, -2, "external");
|
||||
if (track->external_filename) {
|
||||
lua_pushstring(L, track->external_filename);
|
||||
lua_setfield(L, -2, "external_filename");
|
||||
}
|
||||
lua_pushboolean(L, track->auto_loaded);
|
||||
lua_setfield(L, -2, "auto_loaded");
|
||||
|
||||
lua_pushinteger(L, n + 1); // list track n1
|
||||
lua_insert(L, -2); // list n1 track
|
||||
lua_settable(L, -3); // list
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int script_input_define_section(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
char *section = (char *)luaL_checkstring(L, 1);
|
||||
char *contents = (char *)luaL_checkstring(L, 2);
|
||||
mp_input_define_section(mpctx->input, section, "<script>", contents, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int script_input_enable_section(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
char *section = (char *)luaL_checkstring(L, 1);
|
||||
char *sflags = (char *)luaL_optstring(L, 2, "");
|
||||
bstr bflags = bstr0(sflags);
|
||||
int flags = 0;
|
||||
while (bflags.len) {
|
||||
bstr val;
|
||||
bstr_split_tok(bflags, "|", &val, &bflags);
|
||||
if (bstr_equals0(val, "allow-hide-cursor")) {
|
||||
flags |= MP_INPUT_ALLOW_HIDE_CURSOR;
|
||||
} else if (bstr_equals0(val, "allow-vo-dragging")) {
|
||||
flags |= MP_INPUT_ALLOW_VO_DRAGGING;
|
||||
} else if (bstr_equals0(val, "exclusive")) {
|
||||
flags |= MP_INPUT_EXCLUSIVE;
|
||||
} else {
|
||||
luaL_error(L, "invalid flag: '%.*s'", BSTR_P(val));
|
||||
}
|
||||
}
|
||||
mp_input_enable_section(mpctx->input, section, flags);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int script_input_disable_section(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
char *section = (char *)luaL_checkstring(L, 1);
|
||||
mp_input_disable_section(mpctx->input, section);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int script_input_set_section_mouse_area(lua_State *L)
|
||||
{
|
||||
struct MPContext *mpctx = get_mpctx(L);
|
||||
|
||||
double sw, sh;
|
||||
struct osd_object *obj = mpctx->osd->objs[OSDTYPE_EXTERNAL];
|
||||
osd_object_get_scale_factor(mpctx->osd, obj, &sw, &sh);
|
||||
|
||||
char *section = (char *)luaL_checkstring(L, 1);
|
||||
int x0 = luaL_checkinteger(L, 2) / sw;
|
||||
int y0 = luaL_checkinteger(L, 3) / sh;
|
||||
int x1 = luaL_checkinteger(L, 4) / sw;
|
||||
int y1 = luaL_checkinteger(L, 5) / sh;
|
||||
mp_input_set_section_mouse_area(mpctx->input, section, x0, y0, x1, y1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int script_format_time(lua_State *L)
|
||||
{
|
||||
double t = luaL_checknumber(L, 1);
|
||||
const char *fmt = luaL_optstring(L, 2, "%H:%M:%S");
|
||||
char *r = mp_format_time_fmt(fmt, t);
|
||||
if (!r)
|
||||
luaL_error(L, "Invalid time format string '%s'", fmt);
|
||||
lua_pushstring(L, r);
|
||||
talloc_free(r);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct fn_entry {
|
||||
const char *name;
|
||||
int (*fn)(lua_State *L);
|
||||
};
|
||||
|
||||
#define FN_ENTRY(name) {#name, script_ ## name}
|
||||
|
||||
static struct fn_entry fn_list[] = {
|
||||
FN_ENTRY(log),
|
||||
FN_ENTRY(find_config_file),
|
||||
FN_ENTRY(send_command),
|
||||
FN_ENTRY(property_list),
|
||||
FN_ENTRY(set_osd_ass),
|
||||
FN_ENTRY(get_osd_resolution),
|
||||
FN_ENTRY(get_screen_size),
|
||||
FN_ENTRY(get_mouse_pos),
|
||||
FN_ENTRY(get_timer),
|
||||
FN_ENTRY(get_chapter_list),
|
||||
FN_ENTRY(get_track_list),
|
||||
FN_ENTRY(input_define_section),
|
||||
FN_ENTRY(input_enable_section),
|
||||
FN_ENTRY(input_disable_section),
|
||||
FN_ENTRY(input_set_section_mouse_area),
|
||||
FN_ENTRY(format_time),
|
||||
};
|
||||
|
||||
// On stack: mp table
|
||||
static void add_functions(struct script_ctx *ctx)
|
||||
{
|
||||
lua_State *L = ctx->state;
|
||||
|
||||
for (int n = 0; n < MP_ARRAY_SIZE(fn_list); n++) {
|
||||
lua_pushcfunction(L, fn_list[n].fn);
|
||||
lua_setfield(L, -2, fn_list[n].name);
|
||||
}
|
||||
|
||||
lua_pushinteger(L, 0);
|
||||
lua_pushcclosure(L, script_property_string, 1);
|
||||
lua_setfield(L, -2, "property_get");
|
||||
|
||||
lua_pushinteger(L, 1);
|
||||
lua_pushcclosure(L, script_property_string, 1);
|
||||
lua_setfield(L, -2, "property_get_string");
|
||||
}
|
||||
|
||||
void mp_lua_init(struct MPContext *mpctx)
|
||||
{
|
||||
mpctx->lua_ctx = talloc_zero(NULL, struct lua_ctx);
|
||||
// Load scripts from options
|
||||
if (mpctx->opts->lua_load_osc)
|
||||
mp_lua_load_script(mpctx, "@osc");
|
||||
char **files = mpctx->opts->lua_files;
|
||||
for (int n = 0; files && files[n]; n++) {
|
||||
if (files[n][0])
|
||||
mp_lua_load_script(mpctx, files[n]);
|
||||
}
|
||||
}
|
||||
|
||||
void mp_lua_uninit(struct MPContext *mpctx)
|
||||
{
|
||||
if (mpctx->lua_ctx) {
|
||||
while (mpctx->lua_ctx->num_scripts)
|
||||
kill_script(mpctx->lua_ctx->scripts[0]);
|
||||
talloc_free(mpctx->lua_ctx);
|
||||
mpctx->lua_ctx = NULL;
|
||||
}
|
||||
}
|
||||
14
player/mp_lua.h
Normal file
14
player/mp_lua.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef MP_LUA_H
|
||||
#define MP_LUA_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
struct MPContext;
|
||||
|
||||
void mp_lua_init(struct MPContext *mpctx);
|
||||
void mp_lua_uninit(struct MPContext *mpctx);
|
||||
void mp_lua_event(struct MPContext *mpctx, const char *name, const char *arg);
|
||||
void mp_lua_script_dispatch(struct MPContext *mpctx, char *script_name,
|
||||
int id, char *event);
|
||||
|
||||
#endif
|
||||
518
player/osd.c
Normal file
518
player/osd.c
Normal file
@@ -0,0 +1,518 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include <math.h>
|
||||
#include <limits.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "talloc.h"
|
||||
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "mpvcore/options.h"
|
||||
#include "mpvcore/mp_common.h"
|
||||
#include "mpvcore/m_property.h"
|
||||
#include "mpvcore/encode.h"
|
||||
|
||||
#include "osdep/getch2.h"
|
||||
#include "osdep/timer.h"
|
||||
|
||||
#include "demux/demux.h"
|
||||
#include "sub/osd.h"
|
||||
|
||||
#include "mp_core.h"
|
||||
#include "command.h"
|
||||
|
||||
#define saddf(var, ...) (*(var) = talloc_asprintf_append((*var), __VA_ARGS__))
|
||||
|
||||
// append time in the hh:mm:ss format (plus fractions if wanted)
|
||||
static void sadd_hhmmssff(char **buf, double time, bool fractions)
|
||||
{
|
||||
char *s = mp_format_time(time, fractions);
|
||||
*buf = talloc_strdup_append(*buf, s);
|
||||
talloc_free(s);
|
||||
}
|
||||
|
||||
static void sadd_percentage(char **buf, int percent) {
|
||||
if (percent >= 0)
|
||||
*buf = talloc_asprintf_append(*buf, " (%d%%)", percent);
|
||||
}
|
||||
|
||||
static int get_term_width(void)
|
||||
{
|
||||
get_screen_size();
|
||||
int width = screen_width > 0 ? screen_width : 80;
|
||||
#if defined(__MINGW32__) || defined(__CYGWIN__)
|
||||
/* Windows command line is broken (MinGW's rxvt works, but we
|
||||
* should not depend on that). */
|
||||
width--;
|
||||
#endif
|
||||
return width;
|
||||
}
|
||||
|
||||
void write_status_line(struct MPContext *mpctx, const char *line)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
if (opts->slave_mode) {
|
||||
mp_msg(MSGT_STATUSLINE, MSGL_STATUS, "%s\n", line);
|
||||
} else if (erase_to_end_of_line) {
|
||||
mp_msg(MSGT_STATUSLINE, MSGL_STATUS,
|
||||
"%s%s\r", line, erase_to_end_of_line);
|
||||
} else {
|
||||
int pos = strlen(line);
|
||||
int width = get_term_width() - pos;
|
||||
mp_msg(MSGT_STATUSLINE, MSGL_STATUS, "%s%*s\r", line, width, "");
|
||||
}
|
||||
}
|
||||
|
||||
void print_status(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
|
||||
update_window_title(mpctx, false);
|
||||
|
||||
if (opts->quiet)
|
||||
return;
|
||||
|
||||
if (opts->status_msg) {
|
||||
char *r = mp_property_expand_string(mpctx, opts->status_msg);
|
||||
write_status_line(mpctx, r);
|
||||
talloc_free(r);
|
||||
return;
|
||||
}
|
||||
|
||||
char *line = NULL;
|
||||
|
||||
// Playback status
|
||||
if (mpctx->paused_for_cache && !opts->pause) {
|
||||
saddf(&line, "(Buffering) ");
|
||||
} else if (mpctx->paused) {
|
||||
saddf(&line, "(Paused) ");
|
||||
}
|
||||
|
||||
if (mpctx->d_audio)
|
||||
saddf(&line, "A");
|
||||
if (mpctx->d_video)
|
||||
saddf(&line, "V");
|
||||
saddf(&line, ": ");
|
||||
|
||||
// Playback position
|
||||
double cur = get_current_time(mpctx);
|
||||
sadd_hhmmssff(&line, cur, mpctx->opts->osd_fractions);
|
||||
|
||||
double len = get_time_length(mpctx);
|
||||
if (len >= 0) {
|
||||
saddf(&line, " / ");
|
||||
sadd_hhmmssff(&line, len, mpctx->opts->osd_fractions);
|
||||
}
|
||||
|
||||
sadd_percentage(&line, get_percent_pos(mpctx));
|
||||
|
||||
// other
|
||||
if (opts->playback_speed != 1)
|
||||
saddf(&line, " x%4.2f", opts->playback_speed);
|
||||
|
||||
// A-V sync
|
||||
if (mpctx->d_audio && mpctx->d_video && mpctx->sync_audio_to_video) {
|
||||
if (mpctx->last_av_difference != MP_NOPTS_VALUE)
|
||||
saddf(&line, " A-V:%7.3f", mpctx->last_av_difference);
|
||||
else
|
||||
saddf(&line, " A-V: ???");
|
||||
if (fabs(mpctx->total_avsync_change) > 0.05)
|
||||
saddf(&line, " ct:%7.3f", mpctx->total_avsync_change);
|
||||
}
|
||||
|
||||
#if HAVE_ENCODING
|
||||
double position = get_current_pos_ratio(mpctx, true);
|
||||
char lavcbuf[80];
|
||||
if (encode_lavc_getstatus(mpctx->encode_lavc_ctx, lavcbuf, sizeof(lavcbuf),
|
||||
position) >= 0)
|
||||
{
|
||||
// encoding stats
|
||||
saddf(&line, " %s", lavcbuf);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
// VO stats
|
||||
if (mpctx->d_video && mpctx->drop_frame_cnt)
|
||||
saddf(&line, " Late: %d", mpctx->drop_frame_cnt);
|
||||
}
|
||||
|
||||
int cache = mp_get_cache_percent(mpctx);
|
||||
if (cache >= 0)
|
||||
saddf(&line, " Cache: %d%%", cache);
|
||||
|
||||
// end
|
||||
write_status_line(mpctx, line);
|
||||
talloc_free(line);
|
||||
}
|
||||
|
||||
typedef struct mp_osd_msg mp_osd_msg_t;
|
||||
struct mp_osd_msg {
|
||||
/// Previous message on the stack.
|
||||
mp_osd_msg_t *prev;
|
||||
/// Message text.
|
||||
char *msg;
|
||||
int id, level, started;
|
||||
/// Display duration in seconds.
|
||||
double time;
|
||||
// Show full OSD for duration of message instead of msg
|
||||
// (osd_show_progression command)
|
||||
bool show_position;
|
||||
};
|
||||
|
||||
// time is in ms
|
||||
static mp_osd_msg_t *add_osd_msg(struct MPContext *mpctx, int id, int level,
|
||||
int time)
|
||||
{
|
||||
rm_osd_msg(mpctx, id);
|
||||
mp_osd_msg_t *msg = talloc_struct(mpctx, mp_osd_msg_t, {
|
||||
.prev = mpctx->osd_msg_stack,
|
||||
.msg = "",
|
||||
.id = id,
|
||||
.level = level,
|
||||
.time = time / 1000.0,
|
||||
});
|
||||
mpctx->osd_msg_stack = msg;
|
||||
return msg;
|
||||
}
|
||||
|
||||
static void set_osd_msg_va(struct MPContext *mpctx, int id, int level, int time,
|
||||
const char *fmt, va_list ap)
|
||||
{
|
||||
if (level == OSD_LEVEL_INVISIBLE)
|
||||
return;
|
||||
mp_osd_msg_t *msg = add_osd_msg(mpctx, id, level, time);
|
||||
msg->msg = talloc_vasprintf(msg, fmt, ap);
|
||||
}
|
||||
|
||||
void set_osd_msg(struct MPContext *mpctx, int id, int level, int time,
|
||||
const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
set_osd_msg_va(mpctx, id, level, time, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Remove a message from the OSD stack
|
||||
*
|
||||
* This function can be used to get rid of a message right away.
|
||||
*
|
||||
*/
|
||||
|
||||
void rm_osd_msg(struct MPContext *mpctx, int id)
|
||||
{
|
||||
mp_osd_msg_t *msg, *last = NULL;
|
||||
|
||||
// Search for the msg
|
||||
for (msg = mpctx->osd_msg_stack; msg && msg->id != id;
|
||||
last = msg, msg = msg->prev) ;
|
||||
if (!msg)
|
||||
return;
|
||||
|
||||
// Detach it from the stack and free it
|
||||
if (last)
|
||||
last->prev = msg->prev;
|
||||
else
|
||||
mpctx->osd_msg_stack = msg->prev;
|
||||
talloc_free(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Get the current message from the OSD stack.
|
||||
*
|
||||
* This function decrements the message timer and destroys the old ones.
|
||||
* The message that should be displayed is returned (if any).
|
||||
*
|
||||
*/
|
||||
|
||||
static mp_osd_msg_t *get_osd_msg(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
mp_osd_msg_t *msg, *prev, *last = NULL;
|
||||
double now = mp_time_sec();
|
||||
double diff;
|
||||
char hidden_dec_done = 0;
|
||||
|
||||
if (mpctx->osd_visible && now >= mpctx->osd_visible) {
|
||||
mpctx->osd_visible = 0;
|
||||
mpctx->osd->progbar_type = -1; // disable
|
||||
osd_changed(mpctx->osd, OSDTYPE_PROGBAR);
|
||||
}
|
||||
if (mpctx->osd_function_visible && now >= mpctx->osd_function_visible) {
|
||||
mpctx->osd_function_visible = 0;
|
||||
mpctx->osd_function = 0;
|
||||
}
|
||||
|
||||
if (!mpctx->osd_last_update)
|
||||
mpctx->osd_last_update = now;
|
||||
diff = now >= mpctx->osd_last_update ? now - mpctx->osd_last_update : 0;
|
||||
|
||||
mpctx->osd_last_update = now;
|
||||
|
||||
// Look for the first message in the stack with high enough level.
|
||||
for (msg = mpctx->osd_msg_stack; msg; last = msg, msg = prev) {
|
||||
prev = msg->prev;
|
||||
if (msg->level > opts->osd_level && hidden_dec_done)
|
||||
continue;
|
||||
// The message has a high enough level or it is the first hidden one
|
||||
// in both cases we decrement the timer or kill it.
|
||||
if (!msg->started || msg->time > diff) {
|
||||
if (msg->started)
|
||||
msg->time -= diff;
|
||||
else
|
||||
msg->started = 1;
|
||||
// display it
|
||||
if (msg->level <= opts->osd_level)
|
||||
return msg;
|
||||
hidden_dec_done = 1;
|
||||
continue;
|
||||
}
|
||||
// kill the message
|
||||
talloc_free(msg);
|
||||
if (last) {
|
||||
last->prev = prev;
|
||||
msg = last;
|
||||
} else {
|
||||
mpctx->osd_msg_stack = prev;
|
||||
msg = NULL;
|
||||
}
|
||||
}
|
||||
// Nothing found
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// type: mp_osd_font_codepoints, ASCII, or OSD_BAR_*
|
||||
// name: fallback for terminal OSD
|
||||
void set_osd_bar(struct MPContext *mpctx, int type, const char *name,
|
||||
double min, double max, double val)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
if (opts->osd_level < 1 || !opts->osd_bar_visible)
|
||||
return;
|
||||
|
||||
if (mpctx->video_out && opts->term_osd != 1) {
|
||||
mpctx->osd_visible = mp_time_sec() + opts->osd_duration / 1000.0;
|
||||
mpctx->osd->progbar_type = type;
|
||||
mpctx->osd->progbar_value = (val - min) / (max - min);
|
||||
mpctx->osd->progbar_num_stops = 0;
|
||||
osd_changed(mpctx->osd, OSDTYPE_PROGBAR);
|
||||
return;
|
||||
}
|
||||
|
||||
set_osd_msg(mpctx, OSD_MSG_BAR, 1, opts->osd_duration, "%s: %d %%",
|
||||
name, ROUND(100 * (val - min) / (max - min)));
|
||||
}
|
||||
|
||||
// Update a currently displayed bar of the same type, without resetting the
|
||||
// timer.
|
||||
static void update_osd_bar(struct MPContext *mpctx, int type,
|
||||
double min, double max, double val)
|
||||
{
|
||||
if (mpctx->osd->progbar_type == type) {
|
||||
float new_value = (val - min) / (max - min);
|
||||
if (new_value != mpctx->osd->progbar_value) {
|
||||
mpctx->osd->progbar_value = new_value;
|
||||
osd_changed(mpctx->osd, OSDTYPE_PROGBAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void set_osd_bar_chapters(struct MPContext *mpctx, int type)
|
||||
{
|
||||
struct osd_state *osd = mpctx->osd;
|
||||
osd->progbar_num_stops = 0;
|
||||
if (osd->progbar_type == type) {
|
||||
double len = get_time_length(mpctx);
|
||||
if (len > 0) {
|
||||
int num = get_chapter_count(mpctx);
|
||||
for (int n = 0; n < num; n++) {
|
||||
double time = chapter_start_time(mpctx, n);
|
||||
if (time >= 0) {
|
||||
float pos = time / len;
|
||||
MP_TARRAY_APPEND(osd, osd->progbar_stops,
|
||||
osd->progbar_num_stops, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// osd_function is the symbol appearing in the video status, such as OSD_PLAY
|
||||
void set_osd_function(struct MPContext *mpctx, int osd_function)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
|
||||
mpctx->osd_function = osd_function;
|
||||
mpctx->osd_function_visible = mp_time_sec() + opts->osd_duration / 1000.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Display text subtitles on the OSD
|
||||
*/
|
||||
void set_osd_subtitle(struct MPContext *mpctx, const char *text)
|
||||
{
|
||||
if (!text)
|
||||
text = "";
|
||||
if (strcmp(mpctx->osd->sub_text, text) != 0) {
|
||||
osd_set_sub(mpctx->osd, text);
|
||||
if (!mpctx->video_out) {
|
||||
rm_osd_msg(mpctx, OSD_MSG_SUB_BASE);
|
||||
if (text && text[0])
|
||||
set_osd_msg(mpctx, OSD_MSG_SUB_BASE, 1, INT_MAX, "%s", text);
|
||||
}
|
||||
}
|
||||
if (!text[0])
|
||||
rm_osd_msg(mpctx, OSD_MSG_SUB_BASE);
|
||||
}
|
||||
|
||||
// sym == mpctx->osd_function
|
||||
static void saddf_osd_function_sym(char **buffer, int sym)
|
||||
{
|
||||
char temp[10];
|
||||
osd_get_function_sym(temp, sizeof(temp), sym);
|
||||
saddf(buffer, "%s ", temp);
|
||||
}
|
||||
|
||||
static void sadd_osd_status(char **buffer, struct MPContext *mpctx, bool full)
|
||||
{
|
||||
bool fractions = mpctx->opts->osd_fractions;
|
||||
int sym = mpctx->osd_function;
|
||||
if (!sym) {
|
||||
if (mpctx->paused_for_cache && !mpctx->opts->pause) {
|
||||
sym = OSD_CLOCK;
|
||||
} else if (mpctx->paused || mpctx->step_frames) {
|
||||
sym = OSD_PAUSE;
|
||||
} else {
|
||||
sym = OSD_PLAY;
|
||||
}
|
||||
}
|
||||
saddf_osd_function_sym(buffer, sym);
|
||||
char *custom_msg = mpctx->opts->osd_status_msg;
|
||||
if (custom_msg && full) {
|
||||
char *text = mp_property_expand_string(mpctx, custom_msg);
|
||||
*buffer = talloc_strdup_append(*buffer, text);
|
||||
talloc_free(text);
|
||||
} else {
|
||||
sadd_hhmmssff(buffer, get_current_time(mpctx), fractions);
|
||||
if (full) {
|
||||
saddf(buffer, " / ");
|
||||
sadd_hhmmssff(buffer, get_time_length(mpctx), fractions);
|
||||
sadd_percentage(buffer, get_percent_pos(mpctx));
|
||||
int cache = mp_get_cache_percent(mpctx);
|
||||
if (cache >= 0)
|
||||
saddf(buffer, " Cache: %d%%", cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OSD messages initated by seeking commands are added lazily with this
|
||||
// function, because multiple successive seek commands can be coalesced.
|
||||
static void add_seek_osd_messages(struct MPContext *mpctx)
|
||||
{
|
||||
if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_BAR) {
|
||||
double pos = get_current_pos_ratio(mpctx, false);
|
||||
set_osd_bar(mpctx, OSD_BAR_SEEK, "Position", 0, 1, MPCLAMP(pos, 0, 1));
|
||||
set_osd_bar_chapters(mpctx, OSD_BAR_SEEK);
|
||||
}
|
||||
if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_TEXT) {
|
||||
mp_osd_msg_t *msg = add_osd_msg(mpctx, OSD_MSG_TEXT, 1,
|
||||
mpctx->opts->osd_duration);
|
||||
msg->show_position = true;
|
||||
}
|
||||
if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_CHAPTER_TEXT) {
|
||||
char *chapter = chapter_display_name(mpctx, get_current_chapter(mpctx));
|
||||
set_osd_msg(mpctx, OSD_MSG_TEXT, 1, mpctx->opts->osd_duration,
|
||||
"Chapter: %s", chapter);
|
||||
talloc_free(chapter);
|
||||
}
|
||||
if ((mpctx->add_osd_seek_info & OSD_SEEK_INFO_EDITION)
|
||||
&& mpctx->master_demuxer)
|
||||
{
|
||||
set_osd_msg(mpctx, OSD_MSG_TEXT, 1, mpctx->opts->osd_duration,
|
||||
"Playing edition %d of %d.",
|
||||
mpctx->master_demuxer->edition + 1,
|
||||
mpctx->master_demuxer->num_editions);
|
||||
}
|
||||
mpctx->add_osd_seek_info = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Update the OSD message line.
|
||||
*
|
||||
* This function displays the current message on the vo OSD or on the term.
|
||||
* If the stack is empty and the OSD level is high enough the timer
|
||||
* is displayed (only on the vo OSD).
|
||||
*
|
||||
*/
|
||||
|
||||
void update_osd_msg(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
struct osd_state *osd = mpctx->osd;
|
||||
|
||||
add_seek_osd_messages(mpctx);
|
||||
double pos = get_current_pos_ratio(mpctx, false);
|
||||
update_osd_bar(mpctx, OSD_BAR_SEEK, 0, 1, MPCLAMP(pos, 0, 1));
|
||||
|
||||
// Look if we have a msg
|
||||
mp_osd_msg_t *msg = get_osd_msg(mpctx);
|
||||
if (msg && !msg->show_position) {
|
||||
if (mpctx->video_out && opts->term_osd != 1) {
|
||||
osd_set_text(osd, msg->msg);
|
||||
} else if (opts->term_osd) {
|
||||
if (strcmp(mpctx->terminal_osd_text, msg->msg)) {
|
||||
talloc_free(mpctx->terminal_osd_text);
|
||||
mpctx->terminal_osd_text = talloc_strdup(mpctx, msg->msg);
|
||||
// Multi-line message => clear what will be the second line
|
||||
write_status_line(mpctx, "");
|
||||
mp_msg(MSGT_CPLAYER, MSGL_STATUS, "%s%s\n", opts->term_osd_esc,
|
||||
mpctx->terminal_osd_text);
|
||||
print_status(mpctx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int osd_level = opts->osd_level;
|
||||
if (msg && msg->show_position)
|
||||
osd_level = 3;
|
||||
|
||||
if (mpctx->video_out && opts->term_osd != 1) {
|
||||
// fallback on the timer
|
||||
char *text = NULL;
|
||||
|
||||
if (osd_level >= 2)
|
||||
sadd_osd_status(&text, mpctx, osd_level == 3);
|
||||
|
||||
osd_set_text(osd, text);
|
||||
talloc_free(text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the term osd line
|
||||
if (opts->term_osd && mpctx->terminal_osd_text[0]) {
|
||||
mpctx->terminal_osd_text[0] = '\0';
|
||||
mp_msg(MSGT_CPLAYER, MSGL_STATUS, "%s\n", opts->term_osd_esc);
|
||||
}
|
||||
}
|
||||
1343
player/playloop.c
Normal file
1343
player/playloop.c
Normal file
File diff suppressed because it is too large
Load Diff
404
player/screenshot.c
Normal file
404
player/screenshot.c
Normal file
@@ -0,0 +1,404 @@
|
||||
/*
|
||||
* This file is part of mplayer2.
|
||||
*
|
||||
* mplayer2 is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* mplayer2 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with mplayer2; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "osdep/io.h"
|
||||
|
||||
#include "talloc.h"
|
||||
#include "screenshot.h"
|
||||
#include "mp_core.h"
|
||||
#include "command.h"
|
||||
#include "mpvcore/bstr.h"
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "mpvcore/path.h"
|
||||
#include "video/mp_image.h"
|
||||
#include "video/decode/dec_video.h"
|
||||
#include "video/filter/vf.h"
|
||||
#include "video/out/vo.h"
|
||||
#include "video/image_writer.h"
|
||||
#include "sub/osd.h"
|
||||
|
||||
#include "video/csputils.h"
|
||||
|
||||
#define MODE_FULL_WINDOW 1
|
||||
#define MODE_SUBTITLES 2
|
||||
|
||||
typedef struct screenshot_ctx {
|
||||
struct MPContext *mpctx;
|
||||
|
||||
int mode;
|
||||
bool each_frame;
|
||||
bool osd;
|
||||
|
||||
int frameno;
|
||||
} screenshot_ctx;
|
||||
|
||||
void screenshot_init(struct MPContext *mpctx)
|
||||
{
|
||||
mpctx->screenshot_ctx = talloc(mpctx, screenshot_ctx);
|
||||
*mpctx->screenshot_ctx = (screenshot_ctx) {
|
||||
.mpctx = mpctx,
|
||||
.frameno = 1,
|
||||
};
|
||||
}
|
||||
|
||||
#define SMSG_OK 0
|
||||
#define SMSG_ERR 1
|
||||
|
||||
static void screenshot_msg(screenshot_ctx *ctx, int status, const char *msg,
|
||||
...) PRINTF_ATTRIBUTE(3,4);
|
||||
|
||||
static void screenshot_msg(screenshot_ctx *ctx, int status, const char *msg,
|
||||
...)
|
||||
{
|
||||
va_list ap;
|
||||
char *s;
|
||||
|
||||
va_start(ap, msg);
|
||||
s = talloc_vasprintf(NULL, msg, ap);
|
||||
va_end(ap);
|
||||
|
||||
mp_msg(MSGT_CPLAYER, status == SMSG_ERR ? MSGL_ERR : MSGL_INFO, "%s\n", s);
|
||||
if (ctx->osd) {
|
||||
set_osd_msg(ctx->mpctx, OSD_MSG_TEXT, 1, ctx->mpctx->opts->osd_duration,
|
||||
"%s", s);
|
||||
}
|
||||
|
||||
talloc_free(s);
|
||||
}
|
||||
|
||||
static char *stripext(void *talloc_ctx, const char *s)
|
||||
{
|
||||
const char *end = strrchr(s, '.');
|
||||
if (!end)
|
||||
end = s + strlen(s);
|
||||
return talloc_asprintf(talloc_ctx, "%.*s", (int)(end - s), s);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
#define ILLEGAL_FILENAME_CHARS "?\"/\\<>*|:"
|
||||
#else
|
||||
#define ILLEGAL_FILENAME_CHARS "/"
|
||||
#endif
|
||||
|
||||
// Replace all characters disallowed in filenames with '_' and return the newly
|
||||
// allocated result string.
|
||||
static char *sanitize_filename(void *talloc_ctx, const char *s)
|
||||
{
|
||||
char *res = talloc_strdup(talloc_ctx, s);
|
||||
char *cur = res;
|
||||
while (*cur) {
|
||||
if (strchr(ILLEGAL_FILENAME_CHARS, *cur) || ((unsigned char)*cur) < 32)
|
||||
*cur = '_';
|
||||
cur++;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static void append_filename(char **s, const char *f)
|
||||
{
|
||||
char *append = sanitize_filename(NULL, f);
|
||||
*s = talloc_strdup_append(*s, append);
|
||||
talloc_free(append);
|
||||
}
|
||||
|
||||
static char *create_fname(struct MPContext *mpctx, char *template,
|
||||
const char *file_ext, int *sequence, int *frameno)
|
||||
{
|
||||
char *res = talloc_strdup(NULL, ""); //empty string, non-NULL context
|
||||
|
||||
time_t raw_time = time(NULL);
|
||||
struct tm *local_time = localtime(&raw_time);
|
||||
|
||||
if (!template || *template == '\0')
|
||||
template = "shot%n";
|
||||
|
||||
for (;;) {
|
||||
char *next = strchr(template, '%');
|
||||
if (!next)
|
||||
break;
|
||||
res = talloc_strndup_append(res, template, next - template);
|
||||
template = next + 1;
|
||||
char fmt = *template++;
|
||||
switch (fmt) {
|
||||
case '#':
|
||||
case '0':
|
||||
case 'n': {
|
||||
int digits = '4';
|
||||
if (fmt == '#') {
|
||||
if (!*sequence) {
|
||||
*frameno = 1;
|
||||
}
|
||||
fmt = *template++;
|
||||
}
|
||||
if (fmt == '0') {
|
||||
digits = *template++;
|
||||
if (digits < '0' || digits > '9')
|
||||
goto error_exit;
|
||||
fmt = *template++;
|
||||
}
|
||||
if (fmt != 'n')
|
||||
goto error_exit;
|
||||
char fmtstr[] = {'%', '0', digits, 'd', '\0'};
|
||||
res = talloc_asprintf_append(res, fmtstr, *frameno);
|
||||
if (*frameno < 100000 - 1) {
|
||||
(*frameno) += 1;
|
||||
(*sequence) += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'f':
|
||||
case 'F': {
|
||||
char *video_file = mp_basename(mpctx->filename);
|
||||
if (video_file) {
|
||||
char *name = video_file;
|
||||
if (fmt == 'F')
|
||||
name = stripext(res, video_file);
|
||||
append_filename(&res, name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'p':
|
||||
case 'P': {
|
||||
char *t = mp_format_time(get_current_time(mpctx), fmt == 'P');
|
||||
append_filename(&res, t);
|
||||
talloc_free(t);
|
||||
break;
|
||||
}
|
||||
case 'w': {
|
||||
char tfmt = *template;
|
||||
if (!tfmt)
|
||||
goto error_exit;
|
||||
template++;
|
||||
char fmtstr[] = {'%', tfmt, '\0'};
|
||||
char *s = mp_format_time_fmt(fmtstr, get_current_time(mpctx));
|
||||
if (!s)
|
||||
goto error_exit;
|
||||
append_filename(&res, s);
|
||||
talloc_free(s);
|
||||
break;
|
||||
}
|
||||
case 't': {
|
||||
char tfmt = *template;
|
||||
if (!tfmt)
|
||||
goto error_exit;
|
||||
template++;
|
||||
char fmtstr[] = {'%', tfmt, '\0'};
|
||||
char buffer[80];
|
||||
if (strftime(buffer, sizeof(buffer), fmtstr, local_time) == 0)
|
||||
buffer[0] = '\0';
|
||||
append_filename(&res, buffer);
|
||||
break;
|
||||
}
|
||||
case '{': {
|
||||
char *end = strchr(template, '}');
|
||||
if (!end)
|
||||
goto error_exit;
|
||||
struct bstr prop = bstr_splice(bstr0(template), 0, end - template);
|
||||
char *tmp = talloc_asprintf(NULL, "${%.*s}", BSTR_P(prop));
|
||||
char *s = mp_property_expand_string(mpctx, tmp);
|
||||
talloc_free(tmp);
|
||||
if (s)
|
||||
append_filename(&res, s);
|
||||
talloc_free(s);
|
||||
template = end + 1;
|
||||
break;
|
||||
}
|
||||
case '%':
|
||||
res = talloc_strdup_append(res, "%");
|
||||
break;
|
||||
default:
|
||||
goto error_exit;
|
||||
}
|
||||
}
|
||||
|
||||
res = talloc_strdup_append(res, template);
|
||||
res = talloc_asprintf_append(res, ".%s", file_ext);
|
||||
char *fname = mp_get_user_path(NULL, res);
|
||||
talloc_free(res);
|
||||
return fname;
|
||||
|
||||
error_exit:
|
||||
talloc_free(res);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *gen_fname(screenshot_ctx *ctx, const char *file_ext)
|
||||
{
|
||||
int sequence = 0;
|
||||
for (;;) {
|
||||
int prev_sequence = sequence;
|
||||
char *fname = create_fname(ctx->mpctx,
|
||||
ctx->mpctx->opts->screenshot_template,
|
||||
file_ext,
|
||||
&sequence,
|
||||
&ctx->frameno);
|
||||
|
||||
if (!fname) {
|
||||
screenshot_msg(ctx, SMSG_ERR, "Invalid screenshot filename "
|
||||
"template! Fix or remove the --screenshot-template "
|
||||
"option.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!mp_path_exists(fname))
|
||||
return fname;
|
||||
|
||||
if (sequence == prev_sequence) {
|
||||
screenshot_msg(ctx, SMSG_ERR, "Can't save screenshot, file '%s' "
|
||||
"already exists!", fname);
|
||||
talloc_free(fname);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
talloc_free(fname);
|
||||
}
|
||||
}
|
||||
|
||||
static void add_subs(struct MPContext *mpctx, struct mp_image *image)
|
||||
{
|
||||
int d_w = image->display_w ? image->display_w : image->w;
|
||||
int d_h = image->display_h ? image->display_h : image->h;
|
||||
|
||||
double sar = (double)image->w / image->h;
|
||||
double dar = (double)d_w / d_h;
|
||||
struct mp_osd_res res = {
|
||||
.w = image->w,
|
||||
.h = image->h,
|
||||
.display_par = sar / dar,
|
||||
};
|
||||
|
||||
osd_draw_on_image(mpctx->osd, res, mpctx->osd->vo_pts,
|
||||
OSD_DRAW_SUB_ONLY, image);
|
||||
}
|
||||
|
||||
static void screenshot_save(struct MPContext *mpctx, struct mp_image *image)
|
||||
{
|
||||
screenshot_ctx *ctx = mpctx->screenshot_ctx;
|
||||
|
||||
struct image_writer_opts *opts = mpctx->opts->screenshot_image_opts;
|
||||
|
||||
char *filename = gen_fname(ctx, image_writer_file_ext(opts));
|
||||
if (filename) {
|
||||
screenshot_msg(ctx, SMSG_OK, "Screenshot: '%s'", filename);
|
||||
if (!write_image(image, opts, filename))
|
||||
screenshot_msg(ctx, SMSG_ERR, "Error writing screenshot!");
|
||||
talloc_free(filename);
|
||||
}
|
||||
}
|
||||
|
||||
static struct mp_image *screenshot_get(struct MPContext *mpctx, int mode)
|
||||
{
|
||||
struct mp_image *image = NULL;
|
||||
if (mpctx->video_out && mpctx->video_out->config_ok) {
|
||||
if (mode == MODE_SUBTITLES && mpctx->osd->render_subs_in_filter)
|
||||
mode = 0;
|
||||
|
||||
struct voctrl_screenshot_args args =
|
||||
{ .full_window = (mode == MODE_FULL_WINDOW) };
|
||||
|
||||
if (mpctx->d_video && mpctx->d_video->vfilter)
|
||||
vf_control_any(mpctx->d_video->vfilter, VFCTRL_SCREENSHOT, &args);
|
||||
|
||||
if (!args.out_image)
|
||||
vo_control(mpctx->video_out, VOCTRL_SCREENSHOT, &args);
|
||||
|
||||
image = args.out_image;
|
||||
if (image) {
|
||||
if (mode == MODE_SUBTITLES && !args.has_osd)
|
||||
add_subs(mpctx, image);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
void screenshot_to_file(struct MPContext *mpctx, const char *filename, int mode,
|
||||
bool osd)
|
||||
{
|
||||
screenshot_ctx *ctx = mpctx->screenshot_ctx;
|
||||
struct image_writer_opts opts = *mpctx->opts->screenshot_image_opts;
|
||||
bool old_osd = ctx->osd;
|
||||
ctx->osd = osd;
|
||||
|
||||
if (mp_path_exists(filename)) {
|
||||
screenshot_msg(ctx, SMSG_ERR, "Screenshot: file '%s' already exists.",
|
||||
filename);
|
||||
goto end;
|
||||
}
|
||||
char *ext = mp_splitext(filename, NULL);
|
||||
if (ext)
|
||||
opts.format = ext + 1; // omit '.'
|
||||
struct mp_image *image = screenshot_get(mpctx, mode);
|
||||
if (!image) {
|
||||
screenshot_msg(ctx, SMSG_ERR, "Taking screenshot failed.");
|
||||
goto end;
|
||||
}
|
||||
screenshot_msg(ctx, SMSG_OK, "Screenshot: '%s'", filename);
|
||||
if (!write_image(image, &opts, filename))
|
||||
screenshot_msg(ctx, SMSG_ERR, "Error writing screenshot!");
|
||||
talloc_free(image);
|
||||
|
||||
end:
|
||||
ctx->osd = old_osd;
|
||||
}
|
||||
|
||||
void screenshot_request(struct MPContext *mpctx, int mode, bool each_frame,
|
||||
bool osd)
|
||||
{
|
||||
screenshot_ctx *ctx = mpctx->screenshot_ctx;
|
||||
|
||||
if (mode == MODE_SUBTITLES && mpctx->osd->render_subs_in_filter)
|
||||
mode = 0;
|
||||
|
||||
if (each_frame) {
|
||||
ctx->each_frame = !ctx->each_frame;
|
||||
if (!ctx->each_frame)
|
||||
return;
|
||||
} else {
|
||||
ctx->each_frame = false;
|
||||
}
|
||||
|
||||
ctx->mode = mode;
|
||||
ctx->osd = osd;
|
||||
|
||||
struct mp_image *image = screenshot_get(mpctx, mode);
|
||||
|
||||
if (image) {
|
||||
screenshot_save(mpctx, image);
|
||||
} else {
|
||||
screenshot_msg(ctx, SMSG_ERR, "Taking screenshot failed.");
|
||||
}
|
||||
|
||||
talloc_free(image);
|
||||
}
|
||||
|
||||
void screenshot_flip(struct MPContext *mpctx)
|
||||
{
|
||||
screenshot_ctx *ctx = mpctx->screenshot_ctx;
|
||||
|
||||
if (!ctx->each_frame)
|
||||
return;
|
||||
|
||||
ctx->each_frame = false;
|
||||
screenshot_request(mpctx, ctx->mode, true, ctx->osd);
|
||||
}
|
||||
46
player/screenshot.h
Normal file
46
player/screenshot.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* This file is part of mplayer2.
|
||||
*
|
||||
* mplayer2 is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* mplayer2 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with mplayer2; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPLAYER_SCREENSHOT_H
|
||||
#define MPLAYER_SCREENSHOT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
struct MPContext;
|
||||
|
||||
// One time initialization at program start.
|
||||
void screenshot_init(struct MPContext *mpctx);
|
||||
|
||||
// Request a taking & saving a screenshot of the currently displayed frame.
|
||||
// mode: 0: -, 1: save the actual output window contents, 2: with subtitles.
|
||||
// each_frame: If set, this toggles per-frame screenshots, exactly like the
|
||||
// screenshot slave command (MP_CMD_SCREENSHOT).
|
||||
// osd: show status on OSD
|
||||
void screenshot_request(struct MPContext *mpctx, int mode, bool each_frame,
|
||||
bool osd);
|
||||
|
||||
// filename: where to store the screenshot; doesn't try to find an alternate
|
||||
// name if the file already exists
|
||||
// mode, osd: same as in screenshot_request()
|
||||
void screenshot_to_file(struct MPContext *mpctx, const char *filename, int mode,
|
||||
bool osd);
|
||||
|
||||
// Called by the playback core code when a new frame is displayed.
|
||||
void screenshot_flip(struct MPContext *mpctx);
|
||||
|
||||
#endif /* MPLAYER_SCREENSHOT_H */
|
||||
233
player/sub.c
Normal file
233
player/sub.c
Normal file
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include <math.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "talloc.h"
|
||||
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "mpvcore/options.h"
|
||||
#include "mpvcore/mp_common.h"
|
||||
|
||||
#include "stream/stream.h"
|
||||
#include "sub/dec_sub.h"
|
||||
#include "demux/demux.h"
|
||||
#include "video/mp_image.h"
|
||||
#include "video/decode/dec_video.h"
|
||||
|
||||
#include "mp_core.h"
|
||||
|
||||
void uninit_subs(struct demuxer *demuxer)
|
||||
{
|
||||
for (int i = 0; i < demuxer->num_streams; i++) {
|
||||
struct sh_stream *sh = demuxer->streams[i];
|
||||
if (sh->sub) {
|
||||
sub_destroy(sh->sub->dec_sub);
|
||||
sh->sub->dec_sub = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When reading subtitles from a demuxer, and we read video or audio from the
|
||||
// demuxer, we should not explicitly read subtitle packets. (With external
|
||||
// subs, we have to.)
|
||||
static bool is_interleaved(struct MPContext *mpctx, struct track *track)
|
||||
{
|
||||
if (track->is_external || !track->demuxer)
|
||||
return false;
|
||||
|
||||
struct demuxer *demuxer = track->demuxer;
|
||||
for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
|
||||
struct track *other = mpctx->current_track[type];
|
||||
if (other && other != track && other->demuxer && other->demuxer == demuxer)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void reset_subtitles(struct MPContext *mpctx)
|
||||
{
|
||||
if (mpctx->d_sub)
|
||||
sub_reset(mpctx->d_sub);
|
||||
set_osd_subtitle(mpctx, NULL);
|
||||
osd_changed(mpctx->osd, OSDTYPE_SUB);
|
||||
}
|
||||
|
||||
void update_subtitles(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
if (!(mpctx->initialized_flags & INITIALIZED_SUB))
|
||||
return;
|
||||
|
||||
struct track *track = mpctx->current_track[STREAM_SUB];
|
||||
struct dec_sub *dec_sub = mpctx->d_sub;
|
||||
assert(track && dec_sub);
|
||||
|
||||
if (mpctx->d_video) {
|
||||
struct mp_image_params params = mpctx->d_video->vf_input;
|
||||
if (params.imgfmt)
|
||||
sub_control(dec_sub, SD_CTRL_SET_VIDEO_PARAMS, ¶ms);
|
||||
}
|
||||
|
||||
mpctx->osd->video_offset = track->under_timeline ? mpctx->video_offset : 0;
|
||||
|
||||
double refpts_s = mpctx->playback_pts - mpctx->osd->video_offset;
|
||||
double curpts_s = refpts_s + opts->sub_delay;
|
||||
|
||||
if (!track->preloaded && track->stream) {
|
||||
struct sh_stream *sh_stream = track->stream;
|
||||
bool interleaved = is_interleaved(mpctx, track);
|
||||
|
||||
assert(sh_stream->sub->dec_sub == dec_sub);
|
||||
|
||||
while (1) {
|
||||
if (interleaved && !demux_has_packet(sh_stream))
|
||||
break;
|
||||
double subpts_s = demux_get_next_pts(sh_stream);
|
||||
if (!demux_has_packet(sh_stream))
|
||||
break;
|
||||
if (subpts_s > curpts_s) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_DBG2,
|
||||
"Sub early: c_pts=%5.3f s_pts=%5.3f\n",
|
||||
curpts_s, subpts_s);
|
||||
// Libass handled subs can be fed to it in advance
|
||||
if (!sub_accept_packets_in_advance(dec_sub))
|
||||
break;
|
||||
// Try to avoid demuxing whole file at once
|
||||
if (subpts_s > curpts_s + 1 && !interleaved)
|
||||
break;
|
||||
}
|
||||
struct demux_packet *pkt = demux_read_packet(sh_stream);
|
||||
mp_msg(MSGT_CPLAYER, MSGL_V, "Sub: c_pts=%5.3f s_pts=%5.3f "
|
||||
"duration=%5.3f len=%d\n", curpts_s, pkt->pts, pkt->duration,
|
||||
pkt->len);
|
||||
sub_decode(dec_sub, pkt);
|
||||
talloc_free(pkt);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mpctx->osd->render_bitmap_subs || !mpctx->video_out)
|
||||
set_osd_subtitle(mpctx, sub_get_text(dec_sub, curpts_s));
|
||||
}
|
||||
|
||||
static void set_dvdsub_fake_extradata(struct dec_sub *dec_sub, struct stream *st,
|
||||
int width, int height)
|
||||
{
|
||||
if (!st)
|
||||
return;
|
||||
|
||||
struct stream_dvd_info_req info;
|
||||
if (stream_control(st, STREAM_CTRL_GET_DVD_INFO, &info) < 0)
|
||||
return;
|
||||
|
||||
struct mp_csp_params csp = MP_CSP_PARAMS_DEFAULTS;
|
||||
csp.int_bits_in = 8;
|
||||
csp.int_bits_out = 8;
|
||||
float cmatrix[3][4];
|
||||
mp_get_yuv2rgb_coeffs(&csp, cmatrix);
|
||||
|
||||
if (width == 0 || height == 0) {
|
||||
width = 720;
|
||||
height = 480;
|
||||
}
|
||||
|
||||
char *s = NULL;
|
||||
s = talloc_asprintf_append(s, "size: %dx%d\n", width, height);
|
||||
s = talloc_asprintf_append(s, "palette: ");
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int color = info.palette[i];
|
||||
int c[3] = {(color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff};
|
||||
mp_map_int_color(cmatrix, 8, c);
|
||||
color = (c[2] << 16) | (c[1] << 8) | c[0];
|
||||
|
||||
if (i != 0)
|
||||
talloc_asprintf_append(s, ", ");
|
||||
s = talloc_asprintf_append(s, "%06x", color);
|
||||
}
|
||||
s = talloc_asprintf_append(s, "\n");
|
||||
|
||||
sub_set_extradata(dec_sub, s, strlen(s));
|
||||
talloc_free(s);
|
||||
}
|
||||
|
||||
void reinit_subs(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
struct track *track = mpctx->current_track[STREAM_SUB];
|
||||
|
||||
assert(!(mpctx->initialized_flags & INITIALIZED_SUB));
|
||||
|
||||
init_demux_stream(mpctx, STREAM_SUB);
|
||||
struct sh_stream *sh = mpctx->sh[STREAM_SUB];
|
||||
|
||||
// No track selected, or lazily added DVD track (will actually be created
|
||||
// on first sub packet)
|
||||
if (!sh)
|
||||
return;
|
||||
|
||||
if (!sh->sub->dec_sub) {
|
||||
assert(!mpctx->d_sub);
|
||||
sh->sub->dec_sub = sub_create(opts);
|
||||
}
|
||||
|
||||
assert(!mpctx->d_sub || sh->sub->dec_sub == mpctx->d_sub);
|
||||
|
||||
// The decoder is kept in the stream header in order to make ordered
|
||||
// chapters work well.
|
||||
mpctx->d_sub = sh->sub->dec_sub;
|
||||
|
||||
mpctx->initialized_flags |= INITIALIZED_SUB;
|
||||
|
||||
struct dec_sub *dec_sub = mpctx->d_sub;
|
||||
assert(dec_sub);
|
||||
|
||||
if (!sub_is_initialized(dec_sub)) {
|
||||
struct sh_video *sh_video =
|
||||
mpctx->d_video ? mpctx->d_video->header->video : NULL;
|
||||
int w = sh_video ? sh_video->disp_w : 0;
|
||||
int h = sh_video ? sh_video->disp_h : 0;
|
||||
float fps = sh_video ? sh_video->fps : 25;
|
||||
|
||||
set_dvdsub_fake_extradata(dec_sub, track->demuxer->stream, w, h);
|
||||
sub_set_video_res(dec_sub, w, h);
|
||||
sub_set_video_fps(dec_sub, fps);
|
||||
sub_set_ass_renderer(dec_sub, mpctx->ass_library, mpctx->ass_renderer);
|
||||
sub_init_from_sh(dec_sub, sh);
|
||||
|
||||
// Don't do this if the file has video/audio streams. Don't do it even
|
||||
// if it has only sub streams, because reading packets will change the
|
||||
// demuxer position.
|
||||
if (!track->preloaded && track->is_external) {
|
||||
demux_seek(track->demuxer, 0, SEEK_ABSOLUTE);
|
||||
track->preloaded = sub_read_all_packets(dec_sub, sh);
|
||||
}
|
||||
}
|
||||
|
||||
mpctx->osd->dec_sub = dec_sub;
|
||||
|
||||
// Decides whether to use OSD path or normal subtitle rendering path.
|
||||
mpctx->osd->render_bitmap_subs =
|
||||
opts->ass_enabled || !sub_has_get_text(dec_sub);
|
||||
|
||||
reset_subtitles(mpctx);
|
||||
}
|
||||
417
player/timeline/tl_cue.c
Normal file
417
player/timeline/tl_cue.c
Normal file
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
* This file is part of mplayer2.
|
||||
*
|
||||
* mplayer2 is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* mplayer2 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with mplayer2; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "talloc.h"
|
||||
|
||||
#include "player/mp_core.h"
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "demux/demux.h"
|
||||
#include "mpvcore/path.h"
|
||||
#include "mpvcore/bstr.h"
|
||||
#include "mpvcore/mp_common.h"
|
||||
#include "stream/stream.h"
|
||||
|
||||
// used by demuxer_cue.c
|
||||
bool mp_probe_cue(struct bstr data);
|
||||
|
||||
#define SECS_PER_CUE_FRAME (1.0/75.0)
|
||||
|
||||
enum cue_command {
|
||||
CUE_ERROR = -1, // not a valid CUE command, or an unknown extension
|
||||
CUE_EMPTY, // line with whitespace only
|
||||
CUE_UNUSED, // valid CUE command, but ignored by this code
|
||||
CUE_FILE,
|
||||
CUE_TRACK,
|
||||
CUE_INDEX,
|
||||
CUE_TITLE,
|
||||
};
|
||||
|
||||
static const struct {
|
||||
enum cue_command command;
|
||||
const char *text;
|
||||
} cue_command_strings[] = {
|
||||
{ CUE_FILE, "FILE" },
|
||||
{ CUE_TRACK, "TRACK" },
|
||||
{ CUE_INDEX, "INDEX" },
|
||||
{ CUE_TITLE, "TITLE" },
|
||||
{ CUE_UNUSED, "CATALOG" },
|
||||
{ CUE_UNUSED, "CDTEXTFILE" },
|
||||
{ CUE_UNUSED, "FLAGS" },
|
||||
{ CUE_UNUSED, "ISRC" },
|
||||
{ CUE_UNUSED, "PERFORMER" },
|
||||
{ CUE_UNUSED, "POSTGAP" },
|
||||
{ CUE_UNUSED, "PREGAP" },
|
||||
{ CUE_UNUSED, "REM" },
|
||||
{ CUE_UNUSED, "SONGWRITER" },
|
||||
{ CUE_UNUSED, "MESSAGE" },
|
||||
{ -1 },
|
||||
};
|
||||
|
||||
struct cue_track {
|
||||
double pregap_start; // corresponds to INDEX 00
|
||||
double start; // corresponds to INDEX 01
|
||||
struct bstr filename;
|
||||
int source;
|
||||
struct bstr title;
|
||||
};
|
||||
|
||||
static enum cue_command read_cmd(struct bstr *data, struct bstr *out_params)
|
||||
{
|
||||
struct bstr line = bstr_strip_linebreaks(bstr_getline(*data, data));
|
||||
line = bstr_lstrip(line);
|
||||
if (line.len == 0)
|
||||
return CUE_EMPTY;
|
||||
for (int n = 0; cue_command_strings[n].command != -1; n++) {
|
||||
struct bstr name = bstr0(cue_command_strings[n].text);
|
||||
if (bstr_startswith(line, name)) {
|
||||
struct bstr rest = bstr_cut(line, name.len);
|
||||
if (rest.len && !strchr(WHITESPACE, rest.start[0]))
|
||||
continue;
|
||||
if (out_params)
|
||||
*out_params = rest;
|
||||
return cue_command_strings[n].command;
|
||||
}
|
||||
}
|
||||
return CUE_ERROR;
|
||||
}
|
||||
|
||||
static bool eat_char(struct bstr *data, char ch)
|
||||
{
|
||||
if (data->len && data->start[0] == ch) {
|
||||
*data = bstr_cut(*data, 1);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static struct bstr read_quoted(struct bstr *data)
|
||||
{
|
||||
*data = bstr_lstrip(*data);
|
||||
if (!eat_char(data, '"'))
|
||||
return (struct bstr) {0};
|
||||
int end = bstrchr(*data, '"');
|
||||
if (end < 0)
|
||||
return (struct bstr) {0};
|
||||
struct bstr res = bstr_splice(*data, 0, end);
|
||||
*data = bstr_cut(*data, end + 1);
|
||||
return res;
|
||||
}
|
||||
|
||||
// Read a 2 digit unsigned decimal integer.
|
||||
// Return -1 on failure.
|
||||
static int read_int_2(struct bstr *data)
|
||||
{
|
||||
*data = bstr_lstrip(*data);
|
||||
if (data->len && data->start[0] == '-')
|
||||
return -1;
|
||||
struct bstr s = *data;
|
||||
int res = (int)bstrtoll(s, &s, 10);
|
||||
if (data->len == s.len || data->len - s.len > 2)
|
||||
return -1;
|
||||
*data = s;
|
||||
return res;
|
||||
}
|
||||
|
||||
static double read_time(struct bstr *data)
|
||||
{
|
||||
struct bstr s = *data;
|
||||
bool ok = true;
|
||||
double t1 = read_int_2(&s);
|
||||
ok = eat_char(&s, ':') && ok;
|
||||
double t2 = read_int_2(&s);
|
||||
ok = eat_char(&s, ':') && ok;
|
||||
double t3 = read_int_2(&s);
|
||||
ok = ok && t1 >= 0 && t2 >= 0 && t3 >= 0;
|
||||
return ok ? t1 * 60.0 + t2 + t3 * SECS_PER_CUE_FRAME : 0;
|
||||
}
|
||||
|
||||
static struct bstr skip_utf8_bom(struct bstr data)
|
||||
{
|
||||
return bstr_startswith0(data, "\xEF\xBB\xBF") ? bstr_cut(data, 3) : data;
|
||||
}
|
||||
|
||||
// Check if the text in data is most likely CUE data. This is used by the
|
||||
// demuxer code to check the file type.
|
||||
// data is the start of the probed file, possibly cut off at a random point.
|
||||
bool mp_probe_cue(struct bstr data)
|
||||
{
|
||||
bool valid = false;
|
||||
data = skip_utf8_bom(data);
|
||||
for (;;) {
|
||||
enum cue_command cmd = read_cmd(&data, NULL);
|
||||
// End reached. Since the line was most likely cut off, don't use the
|
||||
// result of the last parsing call.
|
||||
if (data.len == 0)
|
||||
break;
|
||||
if (cmd == CUE_ERROR)
|
||||
return false;
|
||||
if (cmd != CUE_EMPTY)
|
||||
valid = true;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
static void add_source(struct MPContext *mpctx, struct demuxer *d)
|
||||
{
|
||||
MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources, d);
|
||||
}
|
||||
|
||||
static bool try_open(struct MPContext *mpctx, char *filename)
|
||||
{
|
||||
struct bstr bfilename = bstr0(filename);
|
||||
// Avoid trying to open itself or another .cue file. Best would be
|
||||
// to check the result of demuxer auto-detection, but the demuxer
|
||||
// API doesn't allow this without opening a full demuxer.
|
||||
if (bstr_case_endswith(bfilename, bstr0(".cue"))
|
||||
|| bstrcasecmp(bstr0(mpctx->demuxer->filename), bfilename) == 0)
|
||||
return false;
|
||||
|
||||
struct stream *s = stream_open(filename, mpctx->opts);
|
||||
if (!s)
|
||||
return false;
|
||||
struct demuxer *d = demux_open(s, NULL, NULL, mpctx->opts);
|
||||
// Since .bin files are raw PCM data with no headers, we have to explicitly
|
||||
// open them. Also, try to avoid to open files that are most likely not .bin
|
||||
// files, as that would only play noise. Checking the file extension is
|
||||
// fragile, but it's about the only way we have.
|
||||
// TODO: maybe also could check if the .bin file is a multiple of the Audio
|
||||
// CD sector size (2352 bytes)
|
||||
if (!d && bstr_case_endswith(bfilename, bstr0(".bin"))) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_WARN, "CUE: Opening as BIN file!\n");
|
||||
d = demux_open(s, "rawaudio", NULL, mpctx->opts);
|
||||
}
|
||||
if (d) {
|
||||
add_source(mpctx, d);
|
||||
return true;
|
||||
}
|
||||
mp_msg(MSGT_CPLAYER, MSGL_ERR, "Could not open source '%s'!\n", filename);
|
||||
free_stream(s);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool open_source(struct MPContext *mpctx, struct bstr filename)
|
||||
{
|
||||
void *ctx = talloc_new(NULL);
|
||||
bool res = false;
|
||||
|
||||
struct bstr dirname = mp_dirname(mpctx->demuxer->filename);
|
||||
|
||||
struct bstr base_filename = bstr0(mp_basename(bstrdup0(ctx, filename)));
|
||||
if (!base_filename.len) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_WARN,
|
||||
"CUE: Invalid audio filename in .cue file!\n");
|
||||
} else {
|
||||
char *fullname = mp_path_join(ctx, dirname, base_filename);
|
||||
if (try_open(mpctx, fullname)) {
|
||||
res = true;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
// Try an audio file with the same name as the .cue file (but different
|
||||
// extension).
|
||||
// Rationale: this situation happens easily if the audio file or both files
|
||||
// are renamed.
|
||||
|
||||
struct bstr cuefile =
|
||||
bstr_strip_ext(bstr0(mp_basename(mpctx->demuxer->filename)));
|
||||
|
||||
DIR *d = opendir(bstrdup0(ctx, dirname));
|
||||
if (!d)
|
||||
goto out;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d))) {
|
||||
char *dename0 = de->d_name;
|
||||
struct bstr dename = bstr0(dename0);
|
||||
if (bstr_case_startswith(dename, cuefile)) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_WARN, "CUE: No useful audio filename "
|
||||
"in .cue file found, trying with '%s' instead!\n",
|
||||
dename0);
|
||||
if (try_open(mpctx, mp_path_join(ctx, dirname, dename))) {
|
||||
res = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
out:
|
||||
talloc_free(ctx);
|
||||
if (!res)
|
||||
mp_msg(MSGT_CPLAYER, MSGL_ERR, "CUE: Could not open audio file!\n");
|
||||
return res;
|
||||
}
|
||||
|
||||
// return length of the source in seconds, or -1 if unknown
|
||||
static double source_get_length(struct demuxer *demuxer)
|
||||
{
|
||||
double get_time_ans;
|
||||
// <= 0 means DEMUXER_CTRL_NOTIMPL or DEMUXER_CTRL_DONTKNOW
|
||||
if (demuxer && demux_control(demuxer, DEMUXER_CTRL_GET_TIME_LENGTH,
|
||||
(void *) &get_time_ans) > 0)
|
||||
{
|
||||
return get_time_ans;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void build_cue_timeline(struct MPContext *mpctx)
|
||||
{
|
||||
void *ctx = talloc_new(NULL);
|
||||
|
||||
struct bstr data = mpctx->demuxer->file_contents;
|
||||
data = skip_utf8_bom(data);
|
||||
|
||||
struct cue_track *tracks = NULL;
|
||||
size_t track_count = 0;
|
||||
|
||||
struct bstr filename = {0};
|
||||
// Global metadata, and copied into new tracks.
|
||||
struct cue_track proto_track = {0};
|
||||
struct cue_track *cur_track = &proto_track;
|
||||
|
||||
while (data.len) {
|
||||
struct bstr param;
|
||||
switch (read_cmd(&data, ¶m)) {
|
||||
case CUE_ERROR:
|
||||
mp_msg(MSGT_CPLAYER, MSGL_ERR, "CUE: error parsing input file!\n");
|
||||
goto out;
|
||||
case CUE_TRACK: {
|
||||
track_count++;
|
||||
tracks = talloc_realloc(ctx, tracks, struct cue_track, track_count);
|
||||
cur_track = &tracks[track_count - 1];
|
||||
*cur_track = proto_track;
|
||||
break;
|
||||
}
|
||||
case CUE_TITLE:
|
||||
cur_track->title = read_quoted(¶m);
|
||||
break;
|
||||
case CUE_INDEX: {
|
||||
int type = read_int_2(¶m);
|
||||
double time = read_time(¶m);
|
||||
if (type == 1) {
|
||||
cur_track->start = time;
|
||||
cur_track->filename = filename;
|
||||
} else if (type == 0) {
|
||||
cur_track->pregap_start = time;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CUE_FILE:
|
||||
// NOTE: FILE comes before TRACK, so don't use cur_track->filename
|
||||
filename = read_quoted(¶m);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (track_count == 0) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_ERR, "CUE: no tracks found!\n");
|
||||
goto out;
|
||||
}
|
||||
|
||||
// Remove duplicate file entries. This might be too sophisticated, since
|
||||
// CUE files usually use either separate files for every single track, or
|
||||
// only one file for all tracks.
|
||||
|
||||
struct bstr *files = 0;
|
||||
size_t file_count = 0;
|
||||
|
||||
for (size_t n = 0; n < track_count; n++) {
|
||||
struct cue_track *track = &tracks[n];
|
||||
track->source = -1;
|
||||
for (size_t file = 0; file < file_count; file++) {
|
||||
if (bstrcmp(files[file], track->filename) == 0) {
|
||||
track->source = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (track->source == -1) {
|
||||
file_count++;
|
||||
files = talloc_realloc(ctx, files, struct bstr, file_count);
|
||||
files[file_count - 1] = track->filename;
|
||||
track->source = file_count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < file_count; i++) {
|
||||
if (!open_source(mpctx, files[i]))
|
||||
goto out;
|
||||
}
|
||||
|
||||
struct timeline_part *timeline = talloc_array_ptrtype(NULL, timeline,
|
||||
track_count + 1);
|
||||
struct chapter *chapters = talloc_array_ptrtype(NULL, chapters,
|
||||
track_count);
|
||||
double starttime = 0;
|
||||
for (int i = 0; i < track_count; i++) {
|
||||
struct demuxer *source = mpctx->sources[1 + tracks[i].source];
|
||||
double duration;
|
||||
if (i + 1 < track_count && tracks[i].source == tracks[i + 1].source) {
|
||||
duration = tracks[i + 1].start - tracks[i].start;
|
||||
} else {
|
||||
duration = source_get_length(source);
|
||||
// Two cases: 1) last track of a single-file cue, or 2) any track of
|
||||
// a multi-file cue. We need to do this for 1) only because the
|
||||
// timeline needs to be terminated with the length of the last
|
||||
// track.
|
||||
duration -= tracks[i].start;
|
||||
}
|
||||
if (duration < 0) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_WARN,
|
||||
"CUE: Can't get duration of source file!\n");
|
||||
// xxx: do something more reasonable
|
||||
duration = 0.0;
|
||||
}
|
||||
timeline[i] = (struct timeline_part) {
|
||||
.start = starttime,
|
||||
.source_start = tracks[i].start,
|
||||
.source = source,
|
||||
};
|
||||
chapters[i] = (struct chapter) {
|
||||
.start = timeline[i].start,
|
||||
// might want to include other metadata here
|
||||
.name = bstrdup0(chapters, tracks[i].title),
|
||||
};
|
||||
starttime += duration;
|
||||
}
|
||||
|
||||
// apparently we need this to give the last part a non-zero length
|
||||
timeline[track_count] = (struct timeline_part) {
|
||||
.start = starttime,
|
||||
// perhaps unused by the timeline code
|
||||
.source_start = 0,
|
||||
.source = timeline[0].source,
|
||||
};
|
||||
|
||||
mpctx->timeline = timeline;
|
||||
// the last part is not included it in the count
|
||||
mpctx->num_timeline_parts = track_count + 1 - 1;
|
||||
mpctx->chapters = chapters;
|
||||
mpctx->num_chapters = track_count;
|
||||
|
||||
out:
|
||||
talloc_free(ctx);
|
||||
}
|
||||
591
player/timeline/tl_matroska.c
Normal file
591
player/timeline/tl_matroska.c
Normal file
@@ -0,0 +1,591 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include <assert.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <libavutil/common.h>
|
||||
|
||||
#include "osdep/io.h"
|
||||
|
||||
#include "talloc.h"
|
||||
|
||||
#include "player/mp_core.h"
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "demux/demux.h"
|
||||
#include "mpvcore/path.h"
|
||||
#include "mpvcore/bstr.h"
|
||||
#include "mpvcore/mp_common.h"
|
||||
#include "mpvcore/playlist.h"
|
||||
#include "mpvcore/playlist_parser.h"
|
||||
#include "stream/stream.h"
|
||||
|
||||
struct find_entry {
|
||||
char *name;
|
||||
int matchlen;
|
||||
off_t size;
|
||||
};
|
||||
|
||||
static int cmp_entry(const void *pa, const void *pb)
|
||||
{
|
||||
const struct find_entry *a = pa, *b = pb;
|
||||
// check "similar" filenames first
|
||||
int matchdiff = b->matchlen - a->matchlen;
|
||||
if (matchdiff)
|
||||
return FFSIGN(matchdiff);
|
||||
// check small files first
|
||||
off_t sizediff = a->size - b->size;
|
||||
if (sizediff)
|
||||
return FFSIGN(sizediff);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char **find_files(const char *original_file, const char *suffix)
|
||||
{
|
||||
void *tmpmem = talloc_new(NULL);
|
||||
char *basename = mp_basename(original_file);
|
||||
struct bstr directory = mp_dirname(original_file);
|
||||
char **results = talloc_size(NULL, 0);
|
||||
char *dir_zero = bstrdup0(tmpmem, directory);
|
||||
DIR *dp = opendir(dir_zero);
|
||||
if (!dp) {
|
||||
talloc_free(tmpmem);
|
||||
return results;
|
||||
}
|
||||
struct find_entry *entries = NULL;
|
||||
struct dirent *ep;
|
||||
int num_results = 0;
|
||||
while ((ep = readdir(dp))) {
|
||||
int suffix_offset = strlen(ep->d_name) - strlen(suffix);
|
||||
// name must end with suffix
|
||||
if (suffix_offset < 0 || strcmp(ep->d_name + suffix_offset, suffix))
|
||||
continue;
|
||||
// don't list the original name
|
||||
if (!strcmp(ep->d_name, basename))
|
||||
continue;
|
||||
|
||||
char *name = mp_path_join(results, directory, bstr0(ep->d_name));
|
||||
char *s1 = ep->d_name;
|
||||
char *s2 = basename;
|
||||
int matchlen = 0;
|
||||
while (*s1 && *s1++ == *s2++)
|
||||
matchlen++;
|
||||
// be a bit more fuzzy about matching the filename
|
||||
matchlen = (matchlen + 3) / 5;
|
||||
|
||||
struct stat statbuf;
|
||||
if (stat(name, &statbuf) != 0)
|
||||
continue;
|
||||
off_t size = statbuf.st_size;
|
||||
|
||||
entries = talloc_realloc(tmpmem, entries, struct find_entry,
|
||||
num_results + 1);
|
||||
entries[num_results] = (struct find_entry) { name, matchlen, size };
|
||||
num_results++;
|
||||
}
|
||||
closedir(dp);
|
||||
// NOTE: maybe should make it compare pointers instead
|
||||
if (entries)
|
||||
qsort(entries, num_results, sizeof(struct find_entry), cmp_entry);
|
||||
results = talloc_realloc(NULL, results, char *, num_results);
|
||||
for (int i = 0; i < num_results; i++) {
|
||||
results[i] = entries[i].name;
|
||||
}
|
||||
talloc_free(tmpmem);
|
||||
return results;
|
||||
}
|
||||
|
||||
static int enable_cache(struct MPContext *mpctx, struct stream **stream,
|
||||
struct demuxer **demuxer, struct demuxer_params *params)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
|
||||
if (opts->stream_cache_size <= 0)
|
||||
return 0;
|
||||
|
||||
char *filename = talloc_strdup(NULL, (*demuxer)->filename);
|
||||
free_demuxer(*demuxer);
|
||||
free_stream(*stream);
|
||||
|
||||
*stream = stream_open(filename, opts);
|
||||
if (!*stream) {
|
||||
talloc_free(filename);
|
||||
return -1;
|
||||
}
|
||||
|
||||
stream_enable_cache_percent(stream,
|
||||
opts->stream_cache_size,
|
||||
opts->stream_cache_def_size,
|
||||
opts->stream_cache_min_percent,
|
||||
opts->stream_cache_seek_min_percent);
|
||||
|
||||
*demuxer = demux_open(*stream, "mkv", params, opts);
|
||||
if (!*demuxer) {
|
||||
talloc_free(filename);
|
||||
free_stream(*stream);
|
||||
return -1;
|
||||
}
|
||||
|
||||
talloc_free(filename);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static bool has_source_request(struct matroska_segment_uid *uids,
|
||||
int num_sources,
|
||||
struct matroska_segment_uid *new_uid)
|
||||
{
|
||||
for (int i = 0; i < num_sources; ++i) {
|
||||
if (demux_matroska_uid_cmp(uids + i, new_uid))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// segment = get Nth segment of a multi-segment file
|
||||
static bool check_file_seg(struct MPContext *mpctx, struct demuxer ***sources,
|
||||
int *num_sources, struct matroska_segment_uid **uids,
|
||||
char *filename, int segment)
|
||||
{
|
||||
bool was_valid = false;
|
||||
struct demuxer_params params = {
|
||||
.matroska_num_wanted_uids = *num_sources,
|
||||
.matroska_wanted_uids = *uids,
|
||||
.matroska_wanted_segment = segment,
|
||||
.matroska_was_valid = &was_valid,
|
||||
};
|
||||
struct stream *s = stream_open(filename, mpctx->opts);
|
||||
if (!s)
|
||||
return false;
|
||||
struct demuxer *d = demux_open(s, "mkv", ¶ms, mpctx->opts);
|
||||
|
||||
if (!d) {
|
||||
free_stream(s);
|
||||
return was_valid;
|
||||
}
|
||||
if (d->type == DEMUXER_TYPE_MATROSKA) {
|
||||
struct matroska_data *m = &d->matroska_data;
|
||||
|
||||
for (int i = 1; i < *num_sources; i++) {
|
||||
struct matroska_segment_uid *uid = *uids + i;
|
||||
if ((*sources)[i])
|
||||
continue;
|
||||
/* Accept the source if the segment uid matches and the edition
|
||||
* either matches or isn't specified. */
|
||||
if (!memcmp(uid->segment, m->uid.segment, 16) &&
|
||||
(!uid->edition || uid->edition == m->uid.edition)) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO, "Match for source %d: %s\n",
|
||||
i, d->filename);
|
||||
|
||||
for (int j = 0; j < m->num_ordered_chapters; j++) {
|
||||
struct matroska_chapter *c = m->ordered_chapters + j;
|
||||
|
||||
if (!c->has_segment_uid)
|
||||
continue;
|
||||
|
||||
if (has_source_request(*uids, *num_sources, &c->uid))
|
||||
continue;
|
||||
|
||||
/* Set the requested segment. */
|
||||
MP_TARRAY_GROW(NULL, *uids, *num_sources);
|
||||
memcpy((*uids) + *num_sources, &c->uid, sizeof(c->uid));
|
||||
|
||||
/* Add a new source slot. */
|
||||
MP_TARRAY_APPEND(NULL, *sources, *num_sources, NULL);
|
||||
}
|
||||
|
||||
if (enable_cache(mpctx, &s, &d, ¶ms) < 0)
|
||||
continue;
|
||||
|
||||
(*sources)[i] = d;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
free_demuxer(d);
|
||||
free_stream(s);
|
||||
return was_valid;
|
||||
}
|
||||
|
||||
static void check_file(struct MPContext *mpctx, struct demuxer ***sources,
|
||||
int *num_sources, struct matroska_segment_uid **uids,
|
||||
char *filename, int first)
|
||||
{
|
||||
for (int segment = first; ; segment++) {
|
||||
if (!check_file_seg(mpctx, sources, num_sources,
|
||||
uids, filename, segment))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool missing(struct demuxer **sources, int num_sources)
|
||||
{
|
||||
for (int i = 0; i < num_sources; i++) {
|
||||
if (!sources[i])
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int find_ordered_chapter_sources(struct MPContext *mpctx,
|
||||
struct demuxer ***sources,
|
||||
int *num_sources,
|
||||
struct matroska_segment_uid **uids)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
void *tmp = talloc_new(NULL);
|
||||
int num_filenames = 0;
|
||||
char **filenames = NULL;
|
||||
if (*num_sources > 1) {
|
||||
char *main_filename = mpctx->demuxer->filename;
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO, "This file references data from "
|
||||
"other sources.\n");
|
||||
if (opts->ordered_chapters_files && opts->ordered_chapters_files[0]) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO, "Loading references from '%s'.\n",
|
||||
opts->ordered_chapters_files);
|
||||
struct playlist *pl =
|
||||
playlist_parse_file(opts->ordered_chapters_files, opts);
|
||||
talloc_steal(tmp, pl);
|
||||
for (struct playlist_entry *e = pl->first; e; e = e->next)
|
||||
MP_TARRAY_APPEND(tmp, filenames, num_filenames, e->filename);
|
||||
} else if (mpctx->demuxer->stream->uncached_type != STREAMTYPE_FILE) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_WARN, "Playback source is not a "
|
||||
"normal disk file. Will not search for related files.\n");
|
||||
} else {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO, "Will scan other files in the "
|
||||
"same directory to find referenced sources.\n");
|
||||
filenames = find_files(main_filename, ".mkv");
|
||||
num_filenames = MP_TALLOC_ELEMS(filenames);
|
||||
talloc_steal(tmp, filenames);
|
||||
}
|
||||
// Possibly get further segments appended to the first segment
|
||||
check_file(mpctx, sources, num_sources, uids, main_filename, 1);
|
||||
}
|
||||
|
||||
int old_source_count;
|
||||
do {
|
||||
old_source_count = *num_sources;
|
||||
for (int i = 0; i < num_filenames; i++) {
|
||||
if (!missing(*sources, *num_sources))
|
||||
break;
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO, "Checking file %s\n", filenames[i]);
|
||||
check_file(mpctx, sources, num_sources, uids, filenames[i], 0);
|
||||
}
|
||||
/* Loop while we have new sources to look for. */
|
||||
} while (old_source_count != *num_sources);
|
||||
|
||||
if (missing(*sources, *num_sources)) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_ERR, "Failed to find ordered chapter part!\n"
|
||||
"There will be parts MISSING from the video!\n");
|
||||
int j = 1;
|
||||
for (int i = 1; i < *num_sources; i++)
|
||||
if ((*sources)[i]) {
|
||||
struct matroska_segment_uid *source_uid = *uids + i;
|
||||
struct matroska_segment_uid *target_uid = *uids + j;
|
||||
(*sources)[j] = (*sources)[i];
|
||||
memmove(target_uid, source_uid, sizeof(*source_uid));
|
||||
j++;
|
||||
}
|
||||
*num_sources = j;
|
||||
}
|
||||
|
||||
talloc_free(tmp);
|
||||
return *num_sources;
|
||||
}
|
||||
|
||||
static int64_t add_timeline_part(struct MPOpts *opts,
|
||||
struct demuxer *source,
|
||||
struct timeline_part **timeline,
|
||||
int *part_count,
|
||||
uint64_t start,
|
||||
uint64_t *last_end_time,
|
||||
uint64_t *starttime)
|
||||
{
|
||||
/* Only add a separate part if the time or file actually changes.
|
||||
* Matroska files have chapter divisions that are redundant from
|
||||
* timeline point of view because the same chapter structure is used
|
||||
* both to specify the timeline and for normal chapter information.
|
||||
* Removing a missing inserted external chapter can also cause this.
|
||||
* We allow for a configurable fudge factor because of files which
|
||||
* specify chapter end times that are one frame too early;
|
||||
* we don't want to try seeking over a one frame gap. */
|
||||
int64_t join_diff = start - *last_end_time;
|
||||
if (*part_count == 0
|
||||
|| FFABS(join_diff) > opts->chapter_merge_threshold * 1e6
|
||||
|| source != (*timeline)[*part_count - 1].source) {
|
||||
struct timeline_part new = {
|
||||
.start = *starttime / 1e9,
|
||||
.source_start = start / 1e9,
|
||||
.source = source,
|
||||
};
|
||||
MP_TARRAY_APPEND(NULL, *timeline, *part_count, new);
|
||||
} else if (*part_count > 0 && join_diff) {
|
||||
/* Chapter was merged at an inexact boundary;
|
||||
* adjust timestamps to match. */
|
||||
mp_msg(MSGT_CPLAYER, MSGL_V, "Merging timeline part %d with "
|
||||
"offset %g ms.\n", *part_count, join_diff / 1e6);
|
||||
*starttime += join_diff;
|
||||
return join_diff;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void account_missing_time(uint64_t *total_time,
|
||||
uint64_t new_time,
|
||||
const char *message)
|
||||
{
|
||||
if (!new_time)
|
||||
return;
|
||||
|
||||
*total_time += new_time;
|
||||
mp_msg(MSGT_CPLAYER, MSGL_HINT,
|
||||
"missing %"PRIu64" nanoseconds: %s\n",
|
||||
new_time, message);
|
||||
}
|
||||
|
||||
static void build_timeline_loop(struct MPOpts *opts,
|
||||
struct demuxer **sources,
|
||||
int num_sources,
|
||||
int current_source,
|
||||
uint64_t *starttime,
|
||||
uint64_t *missing_time,
|
||||
uint64_t *last_end_time,
|
||||
struct timeline_part **timeline,
|
||||
struct chapter *chapters,
|
||||
int *part_count,
|
||||
uint64_t skip,
|
||||
uint64_t limit)
|
||||
{
|
||||
uint64_t local_starttime = 0;
|
||||
struct demuxer *source = sources[current_source];
|
||||
struct matroska_data *m = &source->matroska_data;
|
||||
|
||||
for (int i = 0; i < m->num_ordered_chapters; i++) {
|
||||
struct matroska_chapter *c = m->ordered_chapters + i;
|
||||
uint64_t chapter_length = c->end - c->start;
|
||||
|
||||
/* Fill in the uid with the current one if one isn't requested. */
|
||||
if (!c->has_segment_uid)
|
||||
memcpy(&c->uid, &m->uid, sizeof(c->uid));
|
||||
|
||||
/* "Seek" to the end of the chapter. */
|
||||
local_starttime += chapter_length;
|
||||
|
||||
/* If we're before the start time for the chapter, skip to the next
|
||||
* one. */
|
||||
if (local_starttime <= skip)
|
||||
continue;
|
||||
|
||||
/* Look for the source for this chapter. */
|
||||
for (int j = 0; j < num_sources; j++) {
|
||||
struct demuxer *linked_source = sources[j];
|
||||
struct matroska_data *linked_m = &linked_source->matroska_data;
|
||||
|
||||
/* Skip if the segment or edition isn't acceptable. */
|
||||
if (!demux_matroska_uid_cmp(&c->uid, &linked_m->uid))
|
||||
continue;
|
||||
|
||||
/* TODO: Add option to support recursive chapters when loading
|
||||
* recursive ordered chapter editions? If so, more code will be
|
||||
* needed to add chapters for external non-ordered segment loading
|
||||
* as well since that part is not recursive. */
|
||||
if (!limit) {
|
||||
chapters[i].start = *starttime / 1e9;
|
||||
chapters[i].name = talloc_strdup(chapters, c->name);
|
||||
}
|
||||
|
||||
/* If we're the source or it's a non-ordered edition reference,
|
||||
* just add a timeline part from the source. */
|
||||
if (current_source == j || !linked_m->num_ordered_chapters) {
|
||||
double source_full_length_seconds = demuxer_get_time_length(linked_source);
|
||||
/* Some accuracy lost, but not enough to care. (Over one
|
||||
* million parts, a nanosecond off here could add up to a
|
||||
* millisecond and trigger a false-positive error message, but
|
||||
* if that's your biggest problem at that point,
|
||||
* congratulations. */
|
||||
uint64_t source_full_length = source_full_length_seconds * 1e9;
|
||||
uint64_t source_length = source_full_length - c->start;
|
||||
int64_t join_diff = 0;
|
||||
|
||||
/* If the chapter starts after the end of a source, there's
|
||||
* nothing we can get from it. Instead, mark the entire chapter
|
||||
* as missing and make the chapter length 0. */
|
||||
if (source_full_length <= c->start) {
|
||||
account_missing_time(missing_time, chapter_length,
|
||||
"referenced segment ends before the requested start time");
|
||||
chapter_length = 0;
|
||||
goto found;
|
||||
}
|
||||
|
||||
/* If the source length starting at the chapter start is
|
||||
* shorter than the chapter it is supposed to fill, add the gap
|
||||
* to missing_time. Also, modify the chapter length to be what
|
||||
* we actually have to avoid playing off the end of the file
|
||||
* and not switching to the next source. */
|
||||
if (source_length < chapter_length) {
|
||||
account_missing_time(missing_time, chapter_length - source_length,
|
||||
"referenced segment ends before the requested end time");
|
||||
chapter_length = source_length;
|
||||
}
|
||||
|
||||
join_diff = add_timeline_part(opts, linked_source, timeline, part_count,
|
||||
c->start, last_end_time, starttime);
|
||||
|
||||
/* If we merged two chapters into a single part due to them
|
||||
* being off by a few frames, we need to change the limit to
|
||||
* avoid chopping the end of the intended chapter (the adding
|
||||
* frames case) or showing extra content (the removing frames
|
||||
* case). Also update chapter_length to incorporate the extra
|
||||
* time. */
|
||||
if (limit) {
|
||||
limit += join_diff;
|
||||
chapter_length += join_diff;
|
||||
}
|
||||
/* Otherwise, we have an ordered edition as the source. Since this
|
||||
* can jump around all over the place, we need to build up the
|
||||
* timeline parts for each of its chapters, but not add them as
|
||||
* chapters. */
|
||||
} else {
|
||||
build_timeline_loop(opts, sources, num_sources, j, starttime,
|
||||
missing_time, last_end_time, timeline,
|
||||
chapters, part_count, c->start, c->end);
|
||||
/* The loop call has added time as needed (we can't add it here
|
||||
* due to 'join_diff' in the add_timeline_part function. Since
|
||||
* the time has already been added as needed, the chapter has
|
||||
* an effective 0 length at this point. */
|
||||
chapter_length = 0;
|
||||
}
|
||||
*last_end_time = c->end;
|
||||
goto found;
|
||||
}
|
||||
|
||||
/* We're missing a part of the chapter, so add it to the accounting. */
|
||||
account_missing_time(missing_time, chapter_length,
|
||||
"the source for a chapter could not be found");
|
||||
/* We don't have the source, but don't leave a gap in the timeline for
|
||||
* the source. */
|
||||
chapter_length = 0;
|
||||
found:;
|
||||
*starttime += chapter_length;
|
||||
/* If we're after the limit on this chapter, stop here. */
|
||||
if (limit && local_starttime >= limit) {
|
||||
/* Back up the global start time by the overflow. */
|
||||
*starttime -= local_starttime - limit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* If we stopped before the limit, add up the missing time. */
|
||||
if (local_starttime < limit)
|
||||
account_missing_time(missing_time, limit - local_starttime,
|
||||
"nested ordered chapter segment is shorter than the requested end time");
|
||||
}
|
||||
|
||||
void build_ordered_chapter_timeline(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
|
||||
if (!opts->ordered_chapters) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO, "File uses ordered chapters, but "
|
||||
"you have disabled support for them. Ignoring.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO, "File uses ordered chapters, will build "
|
||||
"edit timeline.\n");
|
||||
|
||||
struct demuxer *demuxer = mpctx->demuxer;
|
||||
struct matroska_data *m = &demuxer->matroska_data;
|
||||
|
||||
// +1 because sources/uid_map[0] is original file even if all chapters
|
||||
// actually use other sources and need separate entries
|
||||
struct demuxer **sources = talloc_zero_array(NULL, struct demuxer *,
|
||||
m->num_ordered_chapters+1);
|
||||
sources[0] = mpctx->demuxer;
|
||||
struct matroska_segment_uid *uids =
|
||||
talloc_zero_array(NULL, struct matroska_segment_uid,
|
||||
m->num_ordered_chapters + 1);
|
||||
int num_sources = 1;
|
||||
memcpy(uids[0].segment, m->uid.segment, 16);
|
||||
uids[0].edition = 0;
|
||||
|
||||
for (int i = 0; i < m->num_ordered_chapters; i++) {
|
||||
struct matroska_chapter *c = m->ordered_chapters + i;
|
||||
/* If there isn't a segment uid, we are the source. If the segment uid
|
||||
* is our segment uid and the edition matches. We can't accept the
|
||||
* "don't care" edition value of 0 since the user may have requested a
|
||||
* non-default edition. */
|
||||
if (!c->has_segment_uid || demux_matroska_uid_cmp(&c->uid, &m->uid))
|
||||
continue;
|
||||
|
||||
if (has_source_request(uids, num_sources, &c->uid))
|
||||
continue;
|
||||
|
||||
memcpy(uids + num_sources, &c->uid, sizeof(c->uid));
|
||||
sources[num_sources] = NULL;
|
||||
num_sources++;
|
||||
}
|
||||
|
||||
num_sources = find_ordered_chapter_sources(mpctx, &sources, &num_sources,
|
||||
&uids);
|
||||
talloc_free(uids);
|
||||
|
||||
struct timeline_part *timeline = talloc_array_ptrtype(NULL, timeline, 0);
|
||||
struct chapter *chapters =
|
||||
talloc_zero_array(NULL, struct chapter, m->num_ordered_chapters);
|
||||
uint64_t starttime = 0;
|
||||
uint64_t missing_time = 0;
|
||||
uint64_t last_end_time = 0;
|
||||
int part_count = 0;
|
||||
build_timeline_loop(opts, sources, num_sources, 0, &starttime,
|
||||
&missing_time, &last_end_time, &timeline,
|
||||
chapters, &part_count, 0, 0);
|
||||
|
||||
if (!part_count) {
|
||||
// None of the parts come from the file itself???
|
||||
talloc_free(sources);
|
||||
talloc_free(timeline);
|
||||
talloc_free(chapters);
|
||||
return;
|
||||
}
|
||||
|
||||
struct timeline_part new = {
|
||||
.start = starttime / 1e9,
|
||||
};
|
||||
MP_TARRAY_APPEND(NULL, timeline, part_count, new);
|
||||
|
||||
/* Ignore anything less than a millisecond when reporting missing time. If
|
||||
* users really notice less than a millisecond missing, maybe this can be
|
||||
* revisited. */
|
||||
if (missing_time >= 1e6)
|
||||
mp_msg(MSGT_CPLAYER, MSGL_ERR, "There are %.3f seconds missing "
|
||||
"from the timeline!\n", missing_time / 1e9);
|
||||
talloc_free(mpctx->sources);
|
||||
mpctx->sources = sources;
|
||||
mpctx->num_sources = num_sources;
|
||||
mpctx->timeline = timeline;
|
||||
mpctx->num_timeline_parts = part_count - 1;
|
||||
mpctx->num_chapters = m->num_ordered_chapters;
|
||||
mpctx->chapters = chapters;
|
||||
}
|
||||
274
player/timeline/tl_mpv_edl.c
Normal file
274
player/timeline/tl_mpv_edl.c
Normal file
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "talloc.h"
|
||||
|
||||
#include "player/mp_core.h"
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "demux/demux.h"
|
||||
#include "mpvcore/path.h"
|
||||
#include "mpvcore/bstr.h"
|
||||
#include "mpvcore/mp_common.h"
|
||||
#include "stream/stream.h"
|
||||
|
||||
struct tl_part {
|
||||
char *filename; // what is stream_open()ed
|
||||
double offset; // offset into the source file
|
||||
double length; // length of the part (-1 if rest of the file)
|
||||
};
|
||||
|
||||
struct tl_parts {
|
||||
struct tl_part *parts;
|
||||
int num_parts;
|
||||
};
|
||||
|
||||
// Parse a time (absolute file time or duration). Currently equivalent to a
|
||||
// number. Return false on failure.
|
||||
static bool parse_time(bstr str, double *out_time)
|
||||
{
|
||||
bstr rest;
|
||||
double time = bstrtod(str, &rest);
|
||||
if (!str.len || rest.len || !isfinite(time))
|
||||
return false;
|
||||
*out_time = time;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Returns a list of parts, or NULL on parse error.
|
||||
* Syntax (without file header or URI prefix):
|
||||
* url ::= <entry> ( (';' | '\n') <entry> )*
|
||||
* entry ::= <param> ( <param> ',' )*
|
||||
* param ::= [<string> '='] (<string> | '%' <number> '%' <bytes>)
|
||||
*/
|
||||
static struct tl_parts *parse_edl(bstr str)
|
||||
{
|
||||
struct tl_parts *tl = talloc_zero(NULL, struct tl_parts);
|
||||
while (str.len) {
|
||||
if (bstr_eatstart0(&str, "#"))
|
||||
bstr_split_tok(str, "\n", &(bstr){0}, &str);
|
||||
if (bstr_eatstart0(&str, "\n") || bstr_eatstart0(&str, ";"))
|
||||
continue;
|
||||
struct tl_part p = { .length = -1 };
|
||||
int nparam = 0;
|
||||
while (1) {
|
||||
bstr name, val;
|
||||
// Check if it's of the form "name=..."
|
||||
int next = bstrcspn(str, "=%,;\n");
|
||||
if (next > 0 && next < str.len && str.start[next] == '=') {
|
||||
name = bstr_splice(str, 0, next);
|
||||
str = bstr_cut(str, next + 1);
|
||||
} else {
|
||||
const char *names[] = {"file", "start", "length"}; // implied name
|
||||
name = bstr0(nparam < 3 ? names[nparam] : "-");
|
||||
}
|
||||
if (bstr_eatstart0(&str, "%")) {
|
||||
int len = bstrtoll(str, &str, 0);
|
||||
if (!bstr_startswith0(str, "%") || (len > str.len - 1))
|
||||
goto error;
|
||||
val = bstr_splice(str, 1, len + 1);
|
||||
str = bstr_cut(str, len + 1);
|
||||
} else {
|
||||
next = bstrcspn(str, ",;\n");
|
||||
val = bstr_splice(str, 0, next);
|
||||
str = bstr_cut(str, next);
|
||||
}
|
||||
// Interpret parameters. Explicitly ignore unknown ones.
|
||||
if (bstr_equals0(name, "file")) {
|
||||
p.filename = bstrto0(tl, val);
|
||||
} else if (bstr_equals0(name, "start")) {
|
||||
if (!parse_time(val, &p.offset))
|
||||
goto error;
|
||||
} else if (bstr_equals0(name, "length")) {
|
||||
if (!parse_time(val, &p.length))
|
||||
goto error;
|
||||
}
|
||||
nparam++;
|
||||
if (!bstr_eatstart0(&str, ","))
|
||||
break;
|
||||
}
|
||||
if (!p.filename)
|
||||
goto error;
|
||||
MP_TARRAY_APPEND(tl, tl->parts, tl->num_parts, p);
|
||||
}
|
||||
if (!tl->num_parts)
|
||||
goto error;
|
||||
return tl;
|
||||
error:
|
||||
talloc_free(tl);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct demuxer *open_file(char *filename, struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
struct demuxer *d = NULL;
|
||||
struct stream *s = stream_open(filename, opts);
|
||||
if (s) {
|
||||
stream_enable_cache_percent(&s,
|
||||
opts->stream_cache_size,
|
||||
opts->stream_cache_def_size,
|
||||
opts->stream_cache_min_percent,
|
||||
opts->stream_cache_seek_min_percent);
|
||||
d = demux_open(s, NULL, NULL, opts);
|
||||
}
|
||||
if (!d) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Could not open source file '%s'.\n",
|
||||
filename);
|
||||
free_stream(s);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
static struct demuxer *open_source(struct MPContext *mpctx, char *filename)
|
||||
{
|
||||
for (int n = 0; n < mpctx->num_sources; n++) {
|
||||
struct demuxer *d = mpctx->sources[n];
|
||||
if (strcmp(d->stream->url, filename) == 0)
|
||||
return d;
|
||||
}
|
||||
struct demuxer *d = open_file(filename, mpctx);
|
||||
if (d)
|
||||
MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources, d);
|
||||
return d;
|
||||
}
|
||||
|
||||
// Append all chapters from src to the chapters array.
|
||||
// Ignore chapters outside of the given time range.
|
||||
static void copy_chapters(struct chapter **chapters, int *num_chapters,
|
||||
struct demuxer *src, double start, double len,
|
||||
double dest_offset)
|
||||
{
|
||||
int count = demuxer_chapter_count(src);
|
||||
for (int n = 0; n < count; n++) {
|
||||
double time = demuxer_chapter_time(src, n);
|
||||
if (time >= start && time <= start + len) {
|
||||
struct chapter ch = {
|
||||
.start = dest_offset + time,
|
||||
.name = talloc_steal(*chapters, demuxer_chapter_name(src, n)),
|
||||
};
|
||||
MP_TARRAY_APPEND(NULL, *chapters, *num_chapters, ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return length of the source in seconds, or -1 if unknown
|
||||
static double source_get_length(struct demuxer *demuxer)
|
||||
{
|
||||
double time;
|
||||
// <= 0 means DEMUXER_CTRL_NOTIMPL or DEMUXER_CTRL_DONTKNOW
|
||||
if (demux_control(demuxer, DEMUXER_CTRL_GET_TIME_LENGTH, &time) <= 0)
|
||||
time = -1;
|
||||
return time;
|
||||
}
|
||||
|
||||
static void build_timeline(struct MPContext *mpctx, struct tl_parts *parts)
|
||||
{
|
||||
struct chapter *chapters = talloc_new(NULL);
|
||||
int num_chapters = 0;
|
||||
struct timeline_part *timeline = talloc_array_ptrtype(NULL, timeline,
|
||||
parts->num_parts + 1);
|
||||
double starttime = 0;
|
||||
for (int n = 0; n < parts->num_parts; n++) {
|
||||
struct tl_part *part = &parts->parts[n];
|
||||
struct demuxer *source = open_source(mpctx, part->filename);
|
||||
if (!source)
|
||||
goto error;
|
||||
|
||||
double len = source_get_length(source);
|
||||
if (len <= 0) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_WARN,
|
||||
"EDL: source file '%s' has unknown duration.\n",
|
||||
part->filename);
|
||||
}
|
||||
|
||||
// Unkown length => use rest of the file. If duration is unknown, make
|
||||
// something up.
|
||||
if (part->length < 0)
|
||||
part->length = (len < 0 ? 1 : len) - part->offset;
|
||||
|
||||
if (len > 0) {
|
||||
double partlen = part->offset + part->length;
|
||||
if (partlen > len) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_WARN, "EDL: entry %d uses %f "
|
||||
"seconds, but file has only %f seconds.\n",
|
||||
n, partlen, len);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a chapter between each file.
|
||||
struct chapter ch = {
|
||||
.start = starttime,
|
||||
.name = talloc_strdup(chapters, part->filename),
|
||||
};
|
||||
MP_TARRAY_APPEND(NULL, chapters, num_chapters, ch);
|
||||
|
||||
// Also copy the source file's chapters for the relevant parts
|
||||
copy_chapters(&chapters, &num_chapters, source, part->offset,
|
||||
part->length, starttime);
|
||||
|
||||
timeline[n] = (struct timeline_part) {
|
||||
.start = starttime,
|
||||
.source_start = part->offset,
|
||||
.source = source,
|
||||
};
|
||||
|
||||
starttime += part->length;
|
||||
}
|
||||
timeline[parts->num_parts] = (struct timeline_part) {.start = starttime};
|
||||
mpctx->timeline = timeline;
|
||||
mpctx->num_timeline_parts = parts->num_parts;
|
||||
mpctx->chapters = chapters;
|
||||
mpctx->num_chapters = num_chapters;
|
||||
return;
|
||||
|
||||
error:
|
||||
talloc_free(timeline);
|
||||
talloc_free(chapters);
|
||||
}
|
||||
|
||||
// For security, don't allow relative or absolute paths, only plain filenames.
|
||||
// Also, make these filenames relative to the edl source file.
|
||||
static void fix_filenames(struct tl_parts *parts, char *source_path)
|
||||
{
|
||||
struct bstr dirname = mp_dirname(source_path);
|
||||
for (int n = 0; n < parts->num_parts; n++) {
|
||||
struct tl_part *part = &parts->parts[n];
|
||||
char *filename = mp_basename(part->filename); // plain filename only
|
||||
part->filename = mp_path_join(parts, dirname, bstr0(filename));
|
||||
}
|
||||
}
|
||||
|
||||
void build_mpv_edl_timeline(struct MPContext *mpctx)
|
||||
{
|
||||
struct tl_parts *parts = parse_edl(mpctx->demuxer->file_contents);
|
||||
if (!parts) {
|
||||
mp_msg(MSGT_CPLAYER, MSGL_ERR, "Error in EDL.\n");
|
||||
return;
|
||||
}
|
||||
// Source is .edl and not edl:// => don't allow arbitrary paths
|
||||
if (mpctx->demuxer->stream->uncached_type != STREAMTYPE_EDL)
|
||||
fix_filenames(parts, mpctx->demuxer->filename);
|
||||
build_timeline(mpctx, parts);
|
||||
talloc_free(parts);
|
||||
}
|
||||
422
player/video.c
Normal file
422
player/video.c
Normal file
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
* This file is part of MPlayer.
|
||||
*
|
||||
* MPlayer is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MPlayer 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with MPlayer; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include <math.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "talloc.h"
|
||||
|
||||
#include "mpvcore/mp_msg.h"
|
||||
#include "mpvcore/options.h"
|
||||
#include "mpvcore/mp_common.h"
|
||||
#include "mpvcore/encode.h"
|
||||
#include "mpvcore/m_property.h"
|
||||
|
||||
#include "audio/out/ao.h"
|
||||
#include "demux/demux.h"
|
||||
#include "stream/stream.h"
|
||||
#include "sub/osd.h"
|
||||
#include "video/hwdec.h"
|
||||
#include "video/filter/vf.h"
|
||||
#include "video/decode/dec_video.h"
|
||||
#include "video/decode/vd.h"
|
||||
#include "video/out/vo.h"
|
||||
|
||||
#include "mp_core.h"
|
||||
#include "command.h"
|
||||
|
||||
void update_fps(struct MPContext *mpctx)
|
||||
{
|
||||
#if HAVE_ENCODING
|
||||
struct dec_video *d_video = mpctx->d_video;
|
||||
if (mpctx->encode_lavc_ctx && d_video)
|
||||
encode_lavc_set_video_fps(mpctx->encode_lavc_ctx, d_video->fps);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void set_allowed_vo_formats(struct vf_chain *c, struct vo *vo)
|
||||
{
|
||||
for (int fmt = IMGFMT_START; fmt < IMGFMT_END; fmt++) {
|
||||
c->allowed_output_formats[fmt - IMGFMT_START] =
|
||||
vo->driver->query_format(vo, fmt);
|
||||
}
|
||||
}
|
||||
|
||||
static void reconfig_video(struct MPContext *mpctx,
|
||||
const struct mp_image_params *params,
|
||||
bool probe_only)
|
||||
{
|
||||
struct dec_video *d_video = mpctx->d_video;
|
||||
|
||||
d_video->decoder_output = *params;
|
||||
|
||||
set_allowed_vo_formats(d_video->vfilter, mpctx->video_out);
|
||||
|
||||
if (video_reconfig_filters(d_video, params) < 0) {
|
||||
// Most video filters don't work with hardware decoding, so this
|
||||
// might be the reason filter reconfig failed.
|
||||
if (!probe_only &&
|
||||
video_vd_control(d_video, VDCTRL_FORCE_HWDEC_FALLBACK, NULL) == CONTROL_OK)
|
||||
{
|
||||
// Fallback active; decoder will return software format next
|
||||
// time. Don't abort video decoding.
|
||||
d_video->vfilter->initialized = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (d_video->vfilter->initialized < 1)
|
||||
return;
|
||||
|
||||
struct mp_image_params p = d_video->vfilter->output_params;
|
||||
const struct vo_driver *info = mpctx->video_out->driver;
|
||||
mp_msg(MSGT_CPLAYER, MSGL_INFO, "VO: [%s] %dx%d => %dx%d %s\n",
|
||||
info->name, p.w, p.h, p.d_w, p.d_h, vo_format_name(p.imgfmt));
|
||||
mp_msg(MSGT_CPLAYER, MSGL_V, "VO: Description: %s\n", info->description);
|
||||
|
||||
int r = vo_reconfig(mpctx->video_out, &p, 0);
|
||||
if (r < 0)
|
||||
d_video->vfilter->initialized = -1;
|
||||
}
|
||||
|
||||
static void recreate_video_filters(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
struct dec_video *d_video = mpctx->d_video;
|
||||
assert(d_video);
|
||||
|
||||
vf_destroy(d_video->vfilter);
|
||||
d_video->vfilter = vf_new(opts);
|
||||
d_video->vfilter->hwdec = &d_video->hwdec_info;
|
||||
|
||||
vf_append_filter_list(d_video->vfilter, opts->vf_settings);
|
||||
|
||||
// for vf_sub
|
||||
vf_control_any(d_video->vfilter, VFCTRL_SET_OSD_OBJ, mpctx->osd);
|
||||
mpctx->osd->render_subs_in_filter
|
||||
= vf_control_any(d_video->vfilter, VFCTRL_INIT_OSD, NULL) == CONTROL_OK;
|
||||
|
||||
set_allowed_vo_formats(d_video->vfilter, mpctx->video_out);
|
||||
}
|
||||
|
||||
int reinit_video_filters(struct MPContext *mpctx)
|
||||
{
|
||||
struct dec_video *d_video = mpctx->d_video;
|
||||
|
||||
if (!d_video || !d_video->decoder_output.imgfmt)
|
||||
return -2;
|
||||
|
||||
recreate_video_filters(mpctx);
|
||||
reconfig_video(mpctx, &d_video->decoder_output, true);
|
||||
|
||||
return d_video->vfilter && d_video->vfilter->initialized > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
int reinit_video_chain(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
assert(!(mpctx->initialized_flags & INITIALIZED_VCODEC));
|
||||
assert(!mpctx->d_video);
|
||||
init_demux_stream(mpctx, STREAM_VIDEO);
|
||||
struct sh_stream *sh = mpctx->sh[STREAM_VIDEO];
|
||||
if (!sh)
|
||||
goto no_video;
|
||||
|
||||
MP_VERBOSE(mpctx, "[V] fourcc:0x%X size:%dx%d fps:%5.3f\n",
|
||||
sh->format,
|
||||
sh->video->disp_w, sh->video->disp_h,
|
||||
sh->video->fps);
|
||||
|
||||
double ar = -1.0;
|
||||
//================== Init VIDEO (codec & libvo) ==========================
|
||||
if (!opts->fixed_vo || !(mpctx->initialized_flags & INITIALIZED_VO)) {
|
||||
mpctx->video_out = init_best_video_out(mpctx->global, mpctx->input,
|
||||
mpctx->encode_lavc_ctx);
|
||||
if (!mpctx->video_out) {
|
||||
MP_FATAL(mpctx, "Error opening/initializing "
|
||||
"the selected video_out (-vo) device.\n");
|
||||
goto err_out;
|
||||
}
|
||||
mpctx->mouse_cursor_visible = true;
|
||||
mpctx->initialized_flags |= INITIALIZED_VO;
|
||||
}
|
||||
|
||||
update_window_title(mpctx, true);
|
||||
|
||||
struct dec_video *d_video = talloc_zero(NULL, struct dec_video);
|
||||
mpctx->d_video = d_video;
|
||||
d_video->opts = mpctx->opts;
|
||||
d_video->header = sh;
|
||||
d_video->fps = sh->video->fps;
|
||||
d_video->vo = mpctx->video_out;
|
||||
mpctx->initialized_flags |= INITIALIZED_VCODEC;
|
||||
|
||||
vo_control(mpctx->video_out, VOCTRL_GET_HWDEC_INFO, &d_video->hwdec_info);
|
||||
|
||||
if (stream_control(sh->demuxer->stream, STREAM_CTRL_GET_ASPECT_RATIO, &ar)
|
||||
!= STREAM_UNSUPPORTED)
|
||||
d_video->stream_aspect = ar;
|
||||
|
||||
recreate_video_filters(mpctx);
|
||||
|
||||
if (!video_init_best_codec(d_video, opts->video_decoders))
|
||||
goto err_out;
|
||||
|
||||
bool saver_state = opts->pause || !opts->stop_screensaver;
|
||||
vo_control(mpctx->video_out, saver_state ? VOCTRL_RESTORE_SCREENSAVER
|
||||
: VOCTRL_KILL_SCREENSAVER, NULL);
|
||||
|
||||
vo_control(mpctx->video_out, mpctx->paused ? VOCTRL_PAUSE
|
||||
: VOCTRL_RESUME, NULL);
|
||||
|
||||
mpctx->restart_playback = true;
|
||||
mpctx->sync_audio_to_video = !sh->attached_picture;
|
||||
mpctx->delay = 0;
|
||||
mpctx->video_next_pts = MP_NOPTS_VALUE;
|
||||
mpctx->playing_last_frame = false;
|
||||
mpctx->last_frame_duration = 0;
|
||||
mpctx->vo_pts_history_seek_ts++;
|
||||
|
||||
vo_seek_reset(mpctx->video_out);
|
||||
reset_subtitles(mpctx);
|
||||
|
||||
if (opts->force_fps) {
|
||||
d_video->fps = opts->force_fps;
|
||||
MP_INFO(mpctx, "FPS forced to be %5.3f.\n", d_video->fps);
|
||||
}
|
||||
if (!sh->video->fps && !opts->force_fps && !opts->correct_pts) {
|
||||
MP_ERR(mpctx, "FPS not specified in the "
|
||||
"header or invalid, use the -fps option.\n");
|
||||
}
|
||||
update_fps(mpctx);
|
||||
|
||||
return 1;
|
||||
|
||||
err_out:
|
||||
no_video:
|
||||
uninit_player(mpctx, INITIALIZED_VCODEC | (opts->force_vo ? 0 : INITIALIZED_VO));
|
||||
cleanup_demux_stream(mpctx, STREAM_VIDEO);
|
||||
mpctx->current_track[STREAM_VIDEO] = NULL;
|
||||
handle_force_window(mpctx, true);
|
||||
MP_INFO(mpctx, "Video: no video\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Try to refresh the video by doing a precise seek to the currently displayed
|
||||
// frame. This can go wrong in all sorts of ways, so use sparingly.
|
||||
void mp_force_video_refresh(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
|
||||
// If not paused, the next frame should come soon enough.
|
||||
if (opts->pause && mpctx->last_vo_pts != MP_NOPTS_VALUE)
|
||||
queue_seek(mpctx, MPSEEK_ABSOLUTE, mpctx->last_vo_pts, 1);
|
||||
}
|
||||
|
||||
static bool filter_output_queued_frame(struct MPContext *mpctx)
|
||||
{
|
||||
struct dec_video *d_video = mpctx->d_video;
|
||||
struct vo *video_out = mpctx->video_out;
|
||||
|
||||
struct mp_image *img = vf_output_queued_frame(d_video->vfilter);
|
||||
if (img)
|
||||
vo_queue_image(video_out, img);
|
||||
talloc_free(img);
|
||||
|
||||
return !!img;
|
||||
}
|
||||
|
||||
static bool load_next_vo_frame(struct MPContext *mpctx, bool eof)
|
||||
{
|
||||
if (vo_get_buffered_frame(mpctx->video_out, eof) >= 0)
|
||||
return true;
|
||||
if (filter_output_queued_frame(mpctx))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Called after video reinit. This can be generally used to try to insert more
|
||||
// filters using the filter chain edit functionality in command.c.
|
||||
static void init_filter_params(struct MPContext *mpctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
|
||||
// Note that the filter chain is already initialized. This code might
|
||||
// recreate the chain a second time, which is not very elegant, but allows
|
||||
// us to test whether enabling deinterlacing works with the current video
|
||||
// format and other filters.
|
||||
if (opts->deinterlace >= 0)
|
||||
mp_property_do("deinterlace", M_PROPERTY_SET, &opts->deinterlace, mpctx);
|
||||
}
|
||||
|
||||
static void filter_video(struct MPContext *mpctx, struct mp_image *frame,
|
||||
bool reconfig_ok)
|
||||
{
|
||||
struct dec_video *d_video = mpctx->d_video;
|
||||
|
||||
struct mp_image_params params;
|
||||
mp_image_params_from_image(¶ms, frame);
|
||||
if (!mp_image_params_equals(&d_video->decoder_output, ¶ms) ||
|
||||
d_video->vfilter->initialized < 1)
|
||||
{
|
||||
// In case we want to wait until filter chain is drained
|
||||
if (!reconfig_ok) {
|
||||
talloc_free(d_video->waiting_decoded_mpi);
|
||||
d_video->waiting_decoded_mpi = frame;
|
||||
return;
|
||||
}
|
||||
|
||||
reconfig_video(mpctx, ¶ms, false);
|
||||
if (d_video->vfilter->initialized > 0)
|
||||
init_filter_params(mpctx);
|
||||
}
|
||||
|
||||
if (d_video->vfilter->initialized < 1) {
|
||||
talloc_free(frame);
|
||||
return;
|
||||
}
|
||||
|
||||
mp_image_set_params(frame, &d_video->vf_input); // force csp/aspect overrides
|
||||
vf_filter_frame(d_video->vfilter, frame);
|
||||
filter_output_queued_frame(mpctx);
|
||||
}
|
||||
|
||||
// Reconfigure the video chain and the VO on a format change. This is separate,
|
||||
// because we wait with the reconfig until the currently buffered video has
|
||||
// finished displaying. Otherwise, we'd resize the window and then wait for the
|
||||
// video finishing, which would result in a black window for that frame.
|
||||
// Does nothing if there was no pending change.
|
||||
void video_execute_format_change(struct MPContext *mpctx)
|
||||
{
|
||||
struct dec_video *d_video = mpctx->d_video;
|
||||
struct mp_image *decoded_frame = d_video->waiting_decoded_mpi;
|
||||
d_video->waiting_decoded_mpi = NULL;
|
||||
if (decoded_frame)
|
||||
filter_video(mpctx, decoded_frame, true);
|
||||
}
|
||||
|
||||
static int check_framedrop(struct MPContext *mpctx, double frame_time)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
// check for frame-drop:
|
||||
if (mpctx->d_audio && !mpctx->ao->untimed &&
|
||||
!demux_stream_eof(mpctx->sh[STREAM_AUDIO]))
|
||||
{
|
||||
float delay = opts->playback_speed * ao_get_delay(mpctx->ao);
|
||||
float d = delay - mpctx->delay;
|
||||
float fps = mpctx->d_video->fps;
|
||||
if (frame_time < 0)
|
||||
frame_time = fps > 0 ? 1.0 / fps : 0;
|
||||
// we should avoid dropping too many frames in sequence unless we
|
||||
// are too late. and we allow 100ms A-V delay here:
|
||||
if (d < -mpctx->dropped_frames * frame_time - 0.100 && !mpctx->paused
|
||||
&& !mpctx->restart_playback) {
|
||||
mpctx->drop_frame_cnt++;
|
||||
mpctx->dropped_frames++;
|
||||
return mpctx->opts->frame_dropping;
|
||||
} else
|
||||
mpctx->dropped_frames = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static double update_video_attached_pic(struct MPContext *mpctx)
|
||||
{
|
||||
struct dec_video *d_video = mpctx->d_video;
|
||||
|
||||
// Try to decode the picture multiple times, until it is displayed.
|
||||
if (mpctx->video_out->hasframe)
|
||||
return -1;
|
||||
|
||||
struct mp_image *decoded_frame =
|
||||
video_decode(d_video, d_video->header->attached_picture, 0);
|
||||
if (decoded_frame)
|
||||
filter_video(mpctx, decoded_frame, true);
|
||||
load_next_vo_frame(mpctx, true);
|
||||
mpctx->video_next_pts = MP_NOPTS_VALUE;
|
||||
return 0;
|
||||
}
|
||||
|
||||
double update_video(struct MPContext *mpctx, double endpts)
|
||||
{
|
||||
struct dec_video *d_video = mpctx->d_video;
|
||||
struct vo *video_out = mpctx->video_out;
|
||||
|
||||
if (d_video->header->attached_picture)
|
||||
return update_video_attached_pic(mpctx);
|
||||
|
||||
if (load_next_vo_frame(mpctx, false)) {
|
||||
// Use currently queued VO frame
|
||||
} else if (d_video->waiting_decoded_mpi) {
|
||||
// Draining on reconfig
|
||||
if (!load_next_vo_frame(mpctx, true))
|
||||
return -1;
|
||||
} else {
|
||||
// Decode a new frame
|
||||
struct demux_packet *pkt = demux_read_packet(d_video->header);
|
||||
if (pkt && pkt->pts != MP_NOPTS_VALUE)
|
||||
pkt->pts += mpctx->video_offset;
|
||||
if ((pkt && pkt->pts >= mpctx->hrseek_pts - .005) ||
|
||||
d_video->has_broken_packet_pts)
|
||||
{
|
||||
mpctx->hrseek_framedrop = false;
|
||||
}
|
||||
int framedrop_type = mpctx->hrseek_active && mpctx->hrseek_framedrop ?
|
||||
1 : check_framedrop(mpctx, -1);
|
||||
struct mp_image *decoded_frame =
|
||||
video_decode(d_video, pkt, framedrop_type);
|
||||
talloc_free(pkt);
|
||||
if (decoded_frame) {
|
||||
filter_video(mpctx, decoded_frame, false);
|
||||
} else if (!pkt) {
|
||||
if (!load_next_vo_frame(mpctx, true))
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Whether the VO has an image queued.
|
||||
// If it does, it will be used to time and display the next frame.
|
||||
if (!video_out->frame_loaded)
|
||||
return 0;
|
||||
|
||||
double pts = video_out->next_pts;
|
||||
if (endpts == MP_NOPTS_VALUE || pts < endpts)
|
||||
add_frame_pts(mpctx, pts);
|
||||
if (mpctx->hrseek_active && pts < mpctx->hrseek_pts - .005) {
|
||||
vo_skip_frame(video_out);
|
||||
return 0;
|
||||
}
|
||||
mpctx->hrseek_active = false;
|
||||
double last_pts = mpctx->video_next_pts;
|
||||
if (last_pts == MP_NOPTS_VALUE)
|
||||
last_pts = pts;
|
||||
double frame_time = pts - last_pts;
|
||||
if (frame_time < 0 || frame_time >= 60) {
|
||||
// Assume a PTS difference >= 60 seconds is a discontinuity.
|
||||
MP_WARN(mpctx, "Jump in video pts: %f -> %f\n", last_pts, pts);
|
||||
frame_time = 0;
|
||||
}
|
||||
mpctx->video_next_pts = pts;
|
||||
if (mpctx->d_audio)
|
||||
mpctx->delay -= frame_time;
|
||||
return frame_time;
|
||||
}
|
||||
Reference in New Issue
Block a user