Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fe9126bba | ||
|
|
22863d6a79 | ||
|
|
5ccd91d639 | ||
|
|
37bf8de258 | ||
|
|
2d3e4fb1b9 | ||
|
|
78cd25388c | ||
|
|
32f0adaeae | ||
|
|
70a7d9d2c5 | ||
|
|
cf14511b1d | ||
|
|
a6d57d654a | ||
|
|
63671a4fbb | ||
|
|
8ace70cfa6 | ||
|
|
1e0edb3e6f | ||
|
|
4ed46bfa25 | ||
|
|
423d47130a | ||
|
|
7a02593dc4 | ||
|
|
220d28b902 | ||
|
|
ff10bd65d6 | ||
|
|
e2895e9684 | ||
|
|
83be06c0ad | ||
|
|
fd7077ade9 | ||
|
|
d40ccc3416 | ||
|
|
57f5ad445d | ||
|
|
a5458a624d | ||
|
|
edf0b93703 | ||
|
|
6bf46eae23 | ||
|
|
774e818371 | ||
|
|
352c00fd08 | ||
|
|
589b38c187 | ||
|
|
62bc8442be | ||
|
|
0edd08a1f6 | ||
|
|
7f5378680b | ||
|
|
4753a92b80 | ||
|
|
584a27e3b8 | ||
|
|
b4abc0a037 | ||
|
|
192318a216 | ||
|
|
822e6e0dd1 | ||
|
|
45b63c78cd | ||
|
|
de47273716 | ||
|
|
7ace4f2115 | ||
|
|
f67dfcbec6 | ||
|
|
6b9f7923af | ||
|
|
c36b1d01c1 | ||
|
|
07222d14f9 | ||
|
|
28fbee7f03 | ||
|
|
b3d2daaa1d | ||
|
|
3437ea23f6 | ||
|
|
16856994e9 | ||
|
|
c22c440c9c | ||
|
|
be42f4c212 | ||
|
|
1b5f127cb0 | ||
|
|
7ea82eb7bc | ||
|
|
3be989dcd0 | ||
|
|
0adc8ee5c1 | ||
|
|
55fe8aab6a | ||
|
|
b2aa388f27 | ||
|
|
fea1dc0cd5 | ||
|
|
8161b47e73 | ||
|
|
f113e03b89 | ||
|
|
630a7b7d82 | ||
|
|
a4e766a77f | ||
|
|
57deaa02d5 | ||
|
|
a6accfdad7 | ||
|
|
8813fe090d | ||
|
|
72ba8515d5 | ||
|
|
21f5d82dc5 | ||
|
|
8c992458c3 | ||
|
|
40149c017c |
No files matched your search
@@ -25,9 +25,10 @@ API changes
|
||||
|
||||
::
|
||||
|
||||
git - the --msg-level option changes its native type from a flat string to
|
||||
a key-value list (setting/reading the option as string still works)
|
||||
--- mpv 0.8.0 is released ---
|
||||
1.14 - add mpv_wait_async_requests()
|
||||
- the --msg-level option changes its native type from a flat string to
|
||||
a key-value list (setting/reading the option as string still works)
|
||||
1.13 - add MPV_EVENT_QUEUE_OVERFLOW
|
||||
1.12 - add class Handle to qthelper.hpp
|
||||
- improve opengl_cb.h API uninitialization behavior, and fix the qml
|
||||
|
||||
@@ -30,7 +30,7 @@ int main(int argc, char *argv[])
|
||||
// Enable default key bindings, so the user can actually interact with
|
||||
// the player (and e.g. close the window).
|
||||
check_error(mpv_set_option_string(ctx, "input-default-bindings", "yes"));
|
||||
mpv_set_option_string(ctx, "input-x11-keyboard", "yes");
|
||||
mpv_set_option_string(ctx, "input-vo-keyboard", "yes");
|
||||
int val = 1;
|
||||
check_error(mpv_set_option(ctx, "osc", MPV_FORMAT_FLAG, &val));
|
||||
|
||||
|
||||
+33
-3
@@ -4,9 +4,7 @@ JSON IPC
|
||||
mpv can be controlled by external programs using the JSON-based IPC protocol. It
|
||||
can be enabled by specifying the path to a unix socket using the option
|
||||
``--input-unix-socket``. Clients can connect to this socket and send commands to
|
||||
the player or receive events from it. You can use the ``socat`` tool to send
|
||||
commands (and receive reply) from the shell. See the ``--idle`` option how to
|
||||
make mpv start without exiting immediately or playing a file.
|
||||
the player or receive events from it.
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -16,6 +14,38 @@ make mpv start without exiting immediately or playing a file.
|
||||
which can run arbitrary system commands. The use-case is controlling the
|
||||
player locally. This is not different from the MPlayer slave protocol.
|
||||
|
||||
Socat example
|
||||
-------------
|
||||
|
||||
You can use the ``socat`` tool to send commands (and receive reply) from the
|
||||
shell. Assuming mpv was started with:
|
||||
|
||||
::
|
||||
|
||||
mpv file.mkv --input-unix-socket=/tmp/mpvsocket
|
||||
|
||||
Then you can control it using socat:
|
||||
|
||||
::
|
||||
|
||||
> echo '{ "command": ["get_property", "playback-time"] }' | socat - /tmp/mpvsocket
|
||||
{"data":190.482000,"error":"success"}
|
||||
|
||||
In this case, socat copies data between stdin/stdout and the mpv socket
|
||||
connection.
|
||||
|
||||
See the ``--idle`` option how to make mpv start without exiting immediately or
|
||||
playing a file.
|
||||
|
||||
It's also possible to send input.conf style text-only commands:
|
||||
|
||||
::
|
||||
|
||||
> echo 'show_text ${playback-time}' | socat - /tmp/mpvsocket
|
||||
|
||||
But you won't get a reply over the socket. (This particular command shows the
|
||||
playback time on the player's OSD.)
|
||||
|
||||
Protocol
|
||||
--------
|
||||
|
||||
|
||||
@@ -551,7 +551,8 @@ Video
|
||||
|
||||
The ``vaapi-copy`` mode allows you to use vaapi with any VO. Because
|
||||
this copies the decoded video back to system RAM, it's likely less efficient
|
||||
than the ``vaapi`` mode.
|
||||
than the ``vaapi`` mode. But there are reports that this is actually faster
|
||||
as well, and avoids many issues with ``vaapi``.
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
+2
-2
@@ -143,8 +143,8 @@ Available filters are:
|
||||
Accurate rounding for the vertical scaler, which may be faster or
|
||||
slower than the default rounding.
|
||||
|
||||
:0: Disable accurate rounding (default).
|
||||
:1: Enable accurate rounding.
|
||||
:no: Disable accurate rounding (default).
|
||||
:yes: Enable accurate rounding.
|
||||
|
||||
``dsize[=w:h:aspect-method:r:aspect]``
|
||||
Changes the intended display size/aspect at an arbitrary point in the
|
||||
|
||||
+27
-8
@@ -590,16 +590,27 @@ Available video output drivers are:
|
||||
See ``--osd-color`` option how colors are defined.
|
||||
|
||||
``smoothmotion``
|
||||
Use frame interpolation to reduce stuttering caused by mismatches in
|
||||
video fps and display refresh rate.
|
||||
Reduce stuttering caused by mismatches in video fps and display
|
||||
refresh rate (also known as judder).
|
||||
|
||||
Instead of drawing each frame exactly once, smoothmotion redraws the
|
||||
the OpenGL scene at the display refresh rate. If a vsync is detected
|
||||
to be the one when a frame changes to the next, a linear interpolation
|
||||
of the previous frame with next is shown instead.
|
||||
Instead of drawing source frames for variable durations, smoothmotion
|
||||
will blend frames that overlap the transition between two frames in
|
||||
the source material.
|
||||
|
||||
This means that displaying a 1fps video on a 60hz monitor will blend
|
||||
at most during 1 vsync for each second of playback.
|
||||
For example, a 24 Hz clip played back on a 60 Hz display would normally
|
||||
result in a pattern like this::
|
||||
|
||||
A A A B B C C C D D E E E F F
|
||||
|
||||
which has different lengths, alternating between 3 and 2. This
|
||||
difference in frame duration is what causes judder.
|
||||
|
||||
With smoothmotion enabled, the pattern changes to::
|
||||
|
||||
A A A+B B B C C C+D D D E E E+F F F
|
||||
|
||||
where A+B is a blend of A and B. In this pattern, each frame gets a
|
||||
(consistent) duration of roughly 2.5 - resulting in smooth motion.
|
||||
|
||||
GPU drivers or compositing window managers overriding vsync behavior
|
||||
can lead to bad results. If the framerate is close to or over the
|
||||
@@ -612,6 +623,14 @@ Available video output drivers are:
|
||||
Mix threshold at which interpolation is skipped (default: 0.0 – never
|
||||
skip).
|
||||
|
||||
For example, with a ``smoothmotion-threshold`` of 0.1, if the
|
||||
smoothmotion algorithm would try to blend two frames by a ratio of
|
||||
95% A + 5% B, it would simply display A instead. (Since the
|
||||
distance, 0.05, is lower than the threshold)
|
||||
|
||||
Setting this to 0.5 would be similar to disabling smoothmotion
|
||||
completely, since it would always just display the nearest frame.
|
||||
|
||||
``opengl-hq``
|
||||
Same as ``opengl``, but with default settings for high quality rendering.
|
||||
|
||||
|
||||
+40
-1
@@ -1,3 +1,42 @@
|
||||
Release 0.8.2
|
||||
=============
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
|
||||
- Fix OSD placement accidentally messed up in 0.8.1.
|
||||
|
||||
Release 0.8.1
|
||||
=============
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- DOCS/client-api-changes has been correctly updated for the v0.8.0 release
|
||||
series.
|
||||
- Quality and major perfomance improvements to smoothmotion.
|
||||
- Silence the "[ytdl_hook] WARNING: video doesn't have subtitles" warning.
|
||||
- Other documentation updates.
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
|
||||
- Attempt to fix OpenGL shader compilation on Intel windows drivers. (bug
|
||||
#1536)
|
||||
- Attempt to improve the OpenGL shader's compatibility with GLES2.
|
||||
- (Windows) Fix noise when seeking while using wasapi:exclusive. (bug #1529)
|
||||
- Fix the waf-based build system when used with newer versions of waf with
|
||||
python3.
|
||||
- Documentation fixes. (includes bug #1608)
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- mpv now tries to load '.vtt' subtitles.
|
||||
|
||||
This listing is not complete. A complete changelog can be seen by running
|
||||
`git log v0.8.0..v0.8.1` in the git repository.
|
||||
|
||||
Release 0.8.0
|
||||
=============
|
||||
|
||||
@@ -162,4 +201,4 @@ New features
|
||||
files.
|
||||
|
||||
This listing is not complete. A complete changelog can be seen by running
|
||||
`git log v0.7.0..` in the git repository.
|
||||
`git log v0.7.0..v0.8.0` in the git repository.
|
||||
+68
-20
@@ -113,6 +113,58 @@ exit_label:
|
||||
return;
|
||||
}
|
||||
|
||||
static void thread_resume(struct ao *ao)
|
||||
{
|
||||
struct wasapi_state *state = (struct wasapi_state *)ao->priv;
|
||||
HRESULT hr;
|
||||
|
||||
MP_DBG(state, "Thread Resume\n");
|
||||
UINT32 padding = 0;
|
||||
hr = IAudioClient_GetCurrentPadding(state->pAudioClient, &padding);
|
||||
if (hr != S_OK) {
|
||||
MP_ERR(state, "IAudioClient_GetCurrentPadding returned %s (0x%"PRIx32")\n",
|
||||
wasapi_explain_err(hr), (uint32_t) hr);
|
||||
}
|
||||
|
||||
/* Fill the buffer before starting, but only if there is no audio queued to play. */
|
||||
/* This prevents overfilling the buffer, which leads to problems in exclusive mode */
|
||||
if (padding < (UINT32)state->bufferFrameCount)
|
||||
thread_feed(ao);
|
||||
|
||||
hr = IAudioClient_Start(state->pAudioClient);
|
||||
if (hr != S_OK) {
|
||||
MP_ERR(state, "IAudioClient_Start returned %s (0x%"PRIx32")\n",
|
||||
wasapi_explain_err(hr), (uint32_t) hr);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static void thread_reset(struct ao *ao)
|
||||
{
|
||||
struct wasapi_state *state = (struct wasapi_state *)ao->priv;
|
||||
HRESULT hr;
|
||||
MP_DBG(state, "Thread Reset\n");
|
||||
hr = IAudioClient_Stop(state->pAudioClient);
|
||||
/* we may get S_FALSE if the stream is already stopped */
|
||||
if (hr != S_OK &&
|
||||
hr != S_FALSE) {
|
||||
MP_ERR(state, "IAudioClient_Stop returned: %s (0x%"PRIx32")\n",
|
||||
wasapi_explain_err(hr), (uint32_t) hr);
|
||||
}
|
||||
|
||||
/* we may get S_FALSE if the stream is already reset */
|
||||
hr = IAudioClient_Reset(state->pAudioClient);
|
||||
if (hr != S_OK &&
|
||||
hr != S_FALSE) {
|
||||
MP_ERR(state, "IAudioClient_Reset returned: %s (0x%"PRIx32")\n",
|
||||
wasapi_explain_err(hr), (uint32_t) hr);
|
||||
}
|
||||
|
||||
atomic_store(&state->sample_count, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
static DWORD __stdcall ThreadLoop(void *lpParameter)
|
||||
{
|
||||
struct ao *ao = lpParameter;
|
||||
@@ -126,10 +178,10 @@ static DWORD __stdcall ThreadLoop(void *lpParameter)
|
||||
|
||||
DWORD waitstatus;
|
||||
HANDLE playcontrol[] =
|
||||
{state->hUninit, state->hFeed, state->hForceFeed, NULL};
|
||||
{state->hUninit, state->hFeed, state->hReset, state->hResume, NULL};
|
||||
MP_DBG(ao, "Entering dispatch loop\n");
|
||||
while (true) { /* watch events */
|
||||
waitstatus = MsgWaitForMultipleObjects(3, playcontrol, FALSE, INFINITE,
|
||||
waitstatus = MsgWaitForMultipleObjects(4, playcontrol, FALSE, INFINITE,
|
||||
QS_POSTMESSAGE | QS_SENDMESSAGE);
|
||||
switch (waitstatus) {
|
||||
case WAIT_OBJECT_0: /*shutdown*/
|
||||
@@ -138,16 +190,18 @@ static DWORD __stdcall ThreadLoop(void *lpParameter)
|
||||
case (WAIT_OBJECT_0 + 1): /* feed */
|
||||
thread_feed(ao);
|
||||
break;
|
||||
case (WAIT_OBJECT_0 + 2): /* force feed */
|
||||
thread_feed(ao);
|
||||
SetEvent(state->hFeedDone);
|
||||
case (WAIT_OBJECT_0 + 2): /* reset */
|
||||
thread_reset(ao);
|
||||
break;
|
||||
case (WAIT_OBJECT_0 + 3): /* messages to dispatch (COM marshalling) */
|
||||
case (WAIT_OBJECT_0 + 3): /* resume */
|
||||
thread_resume(ao);
|
||||
break;
|
||||
case (WAIT_OBJECT_0 + 4): /* messages to dispatch (COM marshalling) */
|
||||
MP_DBG(ao, "Dispatch\n");
|
||||
wasapi_dispatch();
|
||||
break;
|
||||
default:
|
||||
MP_ERR(ao, "Unhandled case in thread loop");
|
||||
MP_ERR(ao, "Unhandled case in thread loop\n");
|
||||
goto exit_label;
|
||||
}
|
||||
}
|
||||
@@ -165,8 +219,8 @@ static void closehandles(struct ao *ao)
|
||||
if (state->init_done) CloseHandle(state->init_done);
|
||||
if (state->hUninit) CloseHandle(state->hUninit);
|
||||
if (state->hFeed) CloseHandle(state->hFeed);
|
||||
if (state->hForceFeed) CloseHandle(state->hForceFeed);
|
||||
if (state->hFeedDone) CloseHandle(state->hFeedDone);
|
||||
if (state->hResume) CloseHandle(state->hResume);
|
||||
if (state->hReset) CloseHandle(state->hReset);
|
||||
if (state->threadLoop) CloseHandle(state->threadLoop);
|
||||
}
|
||||
|
||||
@@ -220,10 +274,10 @@ static int init(struct ao *ao)
|
||||
state->init_done = CreateEventW(NULL, FALSE, FALSE, NULL);
|
||||
state->hUninit = CreateEventW(NULL, FALSE, FALSE, NULL);
|
||||
state->hFeed = CreateEventW(NULL, FALSE, FALSE, NULL); /* for wasapi event mode */
|
||||
state->hForceFeed = CreateEventW(NULL, FALSE, FALSE, NULL);
|
||||
state->hFeedDone = CreateEventW(NULL, FALSE, FALSE, NULL);
|
||||
state->hResume = CreateEventW(NULL, FALSE, FALSE, NULL);
|
||||
state->hReset = CreateEventW(NULL, FALSE, FALSE, NULL);
|
||||
if (!state->init_done || !state->hFeed || !state->hUninit ||
|
||||
!state->hForceFeed || !state->hFeedDone)
|
||||
!state->hResume || !state->hReset)
|
||||
{
|
||||
MP_ERR(ao, "Error initing events\n");
|
||||
uninit(ao);
|
||||
@@ -335,19 +389,13 @@ static int control(struct ao *ao, enum aocontrol cmd, void *arg)
|
||||
static void audio_reset(struct ao *ao)
|
||||
{
|
||||
struct wasapi_state *state = (struct wasapi_state *)ao->priv;
|
||||
|
||||
IAudioClient_Stop(state->pAudioClientProxy);
|
||||
IAudioClient_Reset(state->pAudioClientProxy);
|
||||
atomic_store(&state->sample_count, 0);
|
||||
SetEvent(state->hReset);
|
||||
}
|
||||
|
||||
static void audio_resume(struct ao *ao)
|
||||
{
|
||||
struct wasapi_state *state = (struct wasapi_state *)ao->priv;
|
||||
|
||||
SetEvent(state->hForceFeed);
|
||||
WaitForSingleObject(state->hFeedDone, INFINITE);
|
||||
IAudioClient_Start(state->pAudioClientProxy);
|
||||
SetEvent(state->hResume);
|
||||
}
|
||||
|
||||
static void list_devs(struct ao *ao, struct ao_device_list *list)
|
||||
|
||||
@@ -72,15 +72,14 @@ typedef struct wasapi_state {
|
||||
IMMDeviceEnumerator *pEnumerator;
|
||||
|
||||
HANDLE hFeed; /* wasapi event */
|
||||
HANDLE hForceFeed; /* forces writing a buffer (e.g. before audio_resume) */
|
||||
HANDLE hFeedDone; /* set only after a hForceFeed */
|
||||
HANDLE hResume; /* signal audio thread to resume the stream */
|
||||
HANDLE hReset; /* signal audio thread to reset the stream */
|
||||
HANDLE hTask; /* AV thread */
|
||||
DWORD taskIndex; /* AV task ID */
|
||||
WAVEFORMATEXTENSIBLE format;
|
||||
|
||||
/* WASAPI proxy handles, for Single-Threaded Apartment communication.
|
||||
One is needed for each object that's accessed by a different thread. */
|
||||
IAudioClient *pAudioClientProxy;
|
||||
ISimpleAudioVolume *pAudioVolumeProxy;
|
||||
IAudioEndpointVolume *pEndpointVolumeProxy;
|
||||
IAudioSessionControl *pSessionControlProxy;
|
||||
@@ -88,7 +87,6 @@ typedef struct wasapi_state {
|
||||
/* Streams used to marshal the proxy objects. The thread owning the actual objects
|
||||
needs to marshal proxy objects into these streams, and the thread that wants the
|
||||
proxies unmarshals them from here. */
|
||||
IStream *sAudioClient;
|
||||
IStream *sAudioVolume;
|
||||
IStream *sEndpointVolume;
|
||||
IStream *sSessionControl;
|
||||
|
||||
@@ -958,7 +958,6 @@ HRESULT wasapi_setup_proxies(struct wasapi_state *state) {
|
||||
EXIT_ON_ERROR(hr); \
|
||||
} while (0)
|
||||
|
||||
UNMARSHAL(IID_IAudioClient, state->pAudioClientProxy, state->sAudioClient);
|
||||
UNMARSHAL(IID_ISimpleAudioVolume, state->pAudioVolumeProxy, state->sAudioVolume);
|
||||
UNMARSHAL(IID_IAudioEndpointVolume, state->pEndpointVolumeProxy, state->sEndpointVolume);
|
||||
UNMARSHAL(IID_IAudioSessionControl, state->pSessionControlProxy, state->sSessionControl);
|
||||
@@ -973,7 +972,6 @@ exit_label:
|
||||
}
|
||||
|
||||
void wasapi_release_proxies(wasapi_state *state) {
|
||||
SAFE_RELEASE(state->pAudioClientProxy, IUnknown_Release(state->pAudioClientProxy));
|
||||
SAFE_RELEASE(state->pAudioVolumeProxy, IUnknown_Release(state->pAudioVolumeProxy));
|
||||
SAFE_RELEASE(state->pEndpointVolumeProxy, IUnknown_Release(state->pEndpointVolumeProxy));
|
||||
SAFE_RELEASE(state->pSessionControlProxy, IUnknown_Release(state->pSessionControlProxy));
|
||||
@@ -991,7 +989,6 @@ static HRESULT create_proxies(struct wasapi_state *state) {
|
||||
EXIT_ON_ERROR(hr); \
|
||||
} while (0)
|
||||
|
||||
MARSHAL(IID_IAudioClient, state->sAudioClient, state->pAudioClient);
|
||||
MARSHAL(IID_ISimpleAudioVolume, state->sAudioVolume, state->pAudioVolume);
|
||||
MARSHAL(IID_IAudioEndpointVolume, state->sEndpointVolume, state->pEndpointVolume);
|
||||
MARSHAL(IID_IAudioSessionControl, state->sSessionControl, state->pSessionControl);
|
||||
@@ -1004,7 +1001,6 @@ exit_label:
|
||||
}
|
||||
|
||||
static void destroy_proxies(struct wasapi_state *state) {
|
||||
SAFE_RELEASE(state->sAudioClient, IUnknown_Release(state->sAudioClient));
|
||||
SAFE_RELEASE(state->sAudioVolume, IUnknown_Release(state->sAudioVolume));
|
||||
SAFE_RELEASE(state->sEndpointVolume, IUnknown_Release(state->sEndpointVolume));
|
||||
SAFE_RELEASE(state->sSessionControl, IUnknown_Release(state->sSessionControl));
|
||||
|
||||
+4
-4
@@ -744,7 +744,7 @@ static void demux_export_replaygain(demuxer_t *demuxer)
|
||||
static void demux_copy(struct demuxer *dst, struct demuxer *src)
|
||||
{
|
||||
if (src->events & DEMUX_EVENT_INIT) {
|
||||
// Note that we do as shallow copies as possible. We expect the date
|
||||
// Note that we do as shallow copies as possible. We expect the data
|
||||
// that is not-copied (only referenced) to be immutable.
|
||||
// This implies e.g. that no chapters are added after initialization.
|
||||
dst->chapters = src->chapters;
|
||||
@@ -755,7 +755,6 @@ static void demux_copy(struct demuxer *dst, struct demuxer *src)
|
||||
dst->attachments = src->attachments;
|
||||
dst->num_attachments = src->num_attachments;
|
||||
dst->matroska_data = src->matroska_data;
|
||||
dst->file_contents = src->file_contents;
|
||||
dst->playlist = src->playlist;
|
||||
dst->seekable = src->seekable;
|
||||
dst->partially_seekable = src->partially_seekable;
|
||||
@@ -763,6 +762,7 @@ static void demux_copy(struct demuxer *dst, struct demuxer *src)
|
||||
dst->ts_resets_possible = src->ts_resets_possible;
|
||||
dst->rel_seeks = src->rel_seeks;
|
||||
dst->start_time = src->start_time;
|
||||
dst->priv = src->priv;
|
||||
}
|
||||
if (src->events & DEMUX_EVENT_STREAMS) {
|
||||
// The stream structs themselves are immutable.
|
||||
@@ -841,7 +841,6 @@ static struct demuxer *open_given_type(struct mpv_global *global,
|
||||
struct demuxer *demuxer = talloc_ptrtype(NULL, demuxer);
|
||||
*demuxer = (struct demuxer) {
|
||||
.desc = desc,
|
||||
.type = desc->type,
|
||||
.stream = stream,
|
||||
.seekable = stream->seekable,
|
||||
.filepos = -1,
|
||||
@@ -1385,7 +1384,8 @@ struct demux_chapter *demux_copy_chapter_data(struct demux_chapter *c, int num)
|
||||
for (int n = 0; n < num; n++) {
|
||||
new[n] = c[n];
|
||||
new[n].name = talloc_strdup(new, new[n].name);
|
||||
new[n].metadata = mp_tags_dup(new, new[n].metadata);
|
||||
if (new[n].metadata)
|
||||
new[n].metadata = mp_tags_dup(new, new[n].metadata);
|
||||
}
|
||||
return new;
|
||||
}
|
||||
+3
-12
@@ -36,13 +36,6 @@
|
||||
#define MAX_PACKS 16000
|
||||
#define MAX_PACK_BYTES (400 * 1024 * 1024)
|
||||
|
||||
enum demuxer_type {
|
||||
DEMUXER_TYPE_GENERIC = 0,
|
||||
DEMUXER_TYPE_MATROSKA,
|
||||
DEMUXER_TYPE_EDL,
|
||||
DEMUXER_TYPE_CUE,
|
||||
};
|
||||
|
||||
// DEMUXER control commands/answers
|
||||
#define DEMUXER_CTRL_NOTIMPL -1
|
||||
#define DEMUXER_CTRL_DONTKNOW 0
|
||||
@@ -104,6 +97,7 @@ enum demux_event {
|
||||
#define MAX_SH_STREAMS 256
|
||||
|
||||
struct demuxer;
|
||||
struct timeline;
|
||||
|
||||
/**
|
||||
* Demuxer description structure
|
||||
@@ -112,8 +106,6 @@ typedef struct demuxer_desc {
|
||||
const char *name; // Demuxer name, used with -demuxer switch
|
||||
const char *desc; // Displayed to user
|
||||
|
||||
enum demuxer_type type; // optional
|
||||
|
||||
// Return 0 on success, otherwise -1
|
||||
int (*open)(struct demuxer *demuxer, enum demux_check check);
|
||||
// The following functions are all optional
|
||||
@@ -121,6 +113,8 @@ typedef struct demuxer_desc {
|
||||
void (*close)(struct demuxer *demuxer);
|
||||
void (*seek)(struct demuxer *demuxer, double rel_seek_secs, int flags);
|
||||
int (*control)(struct demuxer *demuxer, int cmd, void *arg);
|
||||
// See timeline.c
|
||||
void (*load_timeline)(struct timeline *tl);
|
||||
} demuxer_desc_t;
|
||||
|
||||
typedef struct demux_chapter
|
||||
@@ -184,7 +178,6 @@ typedef struct demuxer {
|
||||
const char *filetype; // format name when not identified by demuxer (libavformat)
|
||||
int64_t filepos; // input stream current pos.
|
||||
char *filename; // same as stream->url
|
||||
enum demuxer_type type;
|
||||
bool seekable;
|
||||
bool partially_seekable; // implies seekable=true
|
||||
double start_time;
|
||||
@@ -211,8 +204,6 @@ typedef struct demuxer {
|
||||
int num_attachments;
|
||||
|
||||
struct matroska_data matroska_data;
|
||||
// for trivial demuxers which just read the whole file for codec to use
|
||||
struct bstr file_contents;
|
||||
|
||||
// If the file is a playlist file
|
||||
struct playlist *playlist;
|
||||
|
||||
+401
-7
@@ -21,15 +21,407 @@
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <dirent.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "talloc.h"
|
||||
|
||||
#include "misc/bstr.h"
|
||||
#include "demux.h"
|
||||
#include "common/msg.h"
|
||||
#include "demux/demux.h"
|
||||
#include "options/path.h"
|
||||
#include "common/common.h"
|
||||
#include "stream/stream.h"
|
||||
|
||||
// timeline/tl_cue.c
|
||||
bool mp_probe_cue(struct bstr s);
|
||||
#include "timeline.h"
|
||||
|
||||
#define PROBE_SIZE 512
|
||||
#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;
|
||||
};
|
||||
|
||||
struct priv {
|
||||
bstr data;
|
||||
};
|
||||
|
||||
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.
|
||||
static 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 timeline *tl, struct demuxer *d)
|
||||
{
|
||||
MP_TARRAY_APPEND(tl, tl->sources, tl->num_sources, d);
|
||||
}
|
||||
|
||||
static bool try_open(struct timeline *tl, 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(tl->demuxer->filename), bfilename) == 0)
|
||||
return false;
|
||||
|
||||
struct stream *s = stream_open(filename, tl->global);
|
||||
if (!s)
|
||||
return false;
|
||||
struct demuxer *d = demux_open(s, NULL, NULL, tl->global);
|
||||
// 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_WARN(tl, "CUE: Opening as BIN file!\n");
|
||||
d = demux_open(s, "rawaudio", NULL, tl->global);
|
||||
}
|
||||
if (d) {
|
||||
add_source(tl, d);
|
||||
return true;
|
||||
}
|
||||
MP_ERR(tl, "Could not open source '%s'!\n", filename);
|
||||
free_stream(s);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool open_source(struct timeline *tl, struct bstr filename)
|
||||
{
|
||||
void *ctx = talloc_new(NULL);
|
||||
bool res = false;
|
||||
|
||||
struct bstr dirname = mp_dirname(tl->demuxer->filename);
|
||||
|
||||
struct bstr base_filename = bstr0(mp_basename(bstrdup0(ctx, filename)));
|
||||
if (!base_filename.len) {
|
||||
MP_WARN(tl, "CUE: Invalid audio filename in .cue file!\n");
|
||||
} else {
|
||||
char *fullname = mp_path_join(ctx, dirname, base_filename);
|
||||
if (try_open(tl, 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(tl->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_WARN(tl, "CUE: No useful audio filename "
|
||||
"in .cue file found, trying with '%s' instead!\n",
|
||||
dename0);
|
||||
if (try_open(tl, mp_path_join(ctx, dirname, dename))) {
|
||||
res = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
out:
|
||||
talloc_free(ctx);
|
||||
if (!res)
|
||||
MP_ERR(tl, "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;
|
||||
}
|
||||
}
|
||||
|
||||
static void build_timeline(struct timeline *tl)
|
||||
{
|
||||
struct priv *p = tl->demuxer->priv;
|
||||
|
||||
void *ctx = talloc_new(NULL);
|
||||
|
||||
add_source(tl, tl->demuxer);
|
||||
|
||||
struct bstr data = p->data;
|
||||
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_ERR(tl, "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_ERR(tl, "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(tl, files[i]))
|
||||
goto out;
|
||||
}
|
||||
|
||||
struct timeline_part *timeline = talloc_array_ptrtype(tl, timeline,
|
||||
track_count + 1);
|
||||
struct demux_chapter *chapters = talloc_array_ptrtype(tl, chapters,
|
||||
track_count);
|
||||
double starttime = 0;
|
||||
for (int i = 0; i < track_count; i++) {
|
||||
struct demuxer *source = tl->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_WARN(tl, "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 demux_chapter) {
|
||||
.pts = 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,
|
||||
};
|
||||
|
||||
tl->parts = timeline;
|
||||
// the last part is not included it in the count
|
||||
tl->num_parts = track_count + 1 - 1;
|
||||
tl->chapters = chapters;
|
||||
tl->num_chapters = track_count;
|
||||
tl->track_layout = tl->parts[0].source;
|
||||
|
||||
out:
|
||||
talloc_free(ctx);
|
||||
}
|
||||
|
||||
static int try_open_file(struct demuxer *demuxer, enum demux_check check)
|
||||
{
|
||||
@@ -39,8 +431,10 @@ static int try_open_file(struct demuxer *demuxer, enum demux_check check)
|
||||
if (d.len < 1 || !mp_probe_cue(d))
|
||||
return -1;
|
||||
}
|
||||
demuxer->file_contents = stream_read_complete(s, demuxer, 1000000);
|
||||
if (demuxer->file_contents.start == NULL)
|
||||
struct priv *p = talloc_zero(demuxer, struct priv);
|
||||
demuxer->priv = p;
|
||||
p->data = stream_read_complete(s, demuxer, 1000000);
|
||||
if (p->data.start == NULL)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
@@ -48,6 +442,6 @@ static int try_open_file(struct demuxer *demuxer, enum demux_check check)
|
||||
const struct demuxer_desc demuxer_desc_cue = {
|
||||
.name = "cue",
|
||||
.desc = "CUE sheet",
|
||||
.type = DEMUXER_TYPE_CUE,
|
||||
.open = try_open_file,
|
||||
.load_timeline = build_timeline,
|
||||
};
|
||||
+289
-7
@@ -21,34 +21,316 @@
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "demux.h"
|
||||
#include "talloc.h"
|
||||
|
||||
#include "player/core.h"
|
||||
#include "common/msg.h"
|
||||
#include "common/global.h"
|
||||
#include "demux/demux.h"
|
||||
#include "options/path.h"
|
||||
#include "misc/bstr.h"
|
||||
#include "common/common.h"
|
||||
#include "stream/stream.h"
|
||||
|
||||
#define HEADER "# mpv EDL v0\n"
|
||||
|
||||
// Note: the real work is handled in tl_mpv_edl.c.
|
||||
struct tl_part {
|
||||
char *filename; // what is stream_open()ed
|
||||
double offset; // offset into the source file
|
||||
bool offset_set;
|
||||
bool chapter_ts;
|
||||
double length; // length of the part (-1 if rest of the file)
|
||||
};
|
||||
|
||||
struct tl_parts {
|
||||
struct tl_part *parts;
|
||||
int num_parts;
|
||||
};
|
||||
|
||||
struct priv {
|
||||
bstr data;
|
||||
};
|
||||
|
||||
// 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;
|
||||
p.offset_set = true;
|
||||
} else if (bstr_equals0(name, "length")) {
|
||||
if (!parse_time(val, &p.length))
|
||||
goto error;
|
||||
} else if (bstr_equals0(name, "timestamps")) {
|
||||
if (bstr_equals0(val, "chapters"))
|
||||
p.chapter_ts = true;
|
||||
}
|
||||
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 timeline *tl)
|
||||
{
|
||||
struct MPOpts *opts = tl->global->opts;
|
||||
struct demuxer *d = NULL;
|
||||
struct stream *s = stream_open(filename, tl->global);
|
||||
if (s) {
|
||||
stream_enable_cache(&s, &opts->stream_cache);
|
||||
d = demux_open(s, NULL, NULL, tl->global);
|
||||
}
|
||||
if (!d) {
|
||||
MP_ERR(tl, "EDL: Could not open source file '%s'.\n", filename);
|
||||
free_stream(s);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
static struct demuxer *open_source(struct timeline *tl, char *filename)
|
||||
{
|
||||
for (int n = 0; n < tl->num_sources; n++) {
|
||||
struct demuxer *d = tl->sources[n];
|
||||
if (strcmp(d->stream->url, filename) == 0)
|
||||
return d;
|
||||
}
|
||||
struct demuxer *d = open_file(filename, tl);
|
||||
if (d)
|
||||
MP_TARRAY_APPEND(tl, tl->sources, tl->num_sources, d);
|
||||
return d;
|
||||
}
|
||||
|
||||
static double demuxer_chapter_time(struct demuxer *demuxer, int n)
|
||||
{
|
||||
if (n < 0 || n >= demuxer->num_chapters)
|
||||
return -1;
|
||||
return demuxer->chapters[n].pts;
|
||||
}
|
||||
|
||||
// Append all chapters from src to the chapters array.
|
||||
// Ignore chapters outside of the given time range.
|
||||
static void copy_chapters(struct demux_chapter **chapters, int *num_chapters,
|
||||
struct demuxer *src, double start, double len,
|
||||
double dest_offset)
|
||||
{
|
||||
for (int n = 0; n < src->num_chapters; n++) {
|
||||
double time = demuxer_chapter_time(src, n);
|
||||
if (time >= start && time <= start + len) {
|
||||
struct demux_chapter ch = {
|
||||
.pts = dest_offset + time - start,
|
||||
.name = talloc_strdup(*chapters, src->chapters[n].name),
|
||||
};
|
||||
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 resolve_timestamps(struct tl_part *part, struct demuxer *demuxer)
|
||||
{
|
||||
if (part->chapter_ts) {
|
||||
double start = demuxer_chapter_time(demuxer, part->offset);
|
||||
double length = part->length;
|
||||
double end = length;
|
||||
if (end >= 0)
|
||||
end = demuxer_chapter_time(demuxer, part->offset + part->length);
|
||||
if (end >= 0 && start >= 0)
|
||||
length = end - start;
|
||||
part->offset = start;
|
||||
part->length = length;
|
||||
}
|
||||
if (!part->offset_set)
|
||||
part->offset = demuxer->start_time;
|
||||
}
|
||||
|
||||
static void build_timeline(struct timeline *tl, struct tl_parts *parts)
|
||||
{
|
||||
tl->parts = talloc_array_ptrtype(tl, tl->parts, 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(tl, part->filename);
|
||||
if (!source)
|
||||
goto error;
|
||||
|
||||
resolve_timestamps(part, source);
|
||||
|
||||
double len = source_get_length(source);
|
||||
if (len > 0) {
|
||||
len += source->start_time;
|
||||
} else {
|
||||
MP_WARN(tl, "EDL: source file '%s' has unknown duration.\n",
|
||||
part->filename);
|
||||
}
|
||||
|
||||
// Unknown 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_WARN(tl, "EDL: entry %d uses %f "
|
||||
"seconds, but file has only %f seconds.\n",
|
||||
n, partlen, len);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a chapter between each file.
|
||||
struct demux_chapter ch = {
|
||||
.pts = starttime,
|
||||
.name = talloc_strdup(tl, part->filename),
|
||||
};
|
||||
MP_TARRAY_APPEND(tl, tl->chapters, tl->num_chapters, ch);
|
||||
|
||||
// Also copy the source file's chapters for the relevant parts
|
||||
copy_chapters(&tl->chapters, &tl->num_chapters, source, part->offset,
|
||||
part->length, starttime);
|
||||
|
||||
tl->parts[n] = (struct timeline_part) {
|
||||
.start = starttime,
|
||||
.source_start = part->offset,
|
||||
.source = source,
|
||||
};
|
||||
|
||||
starttime += part->length;
|
||||
}
|
||||
tl->parts[parts->num_parts] = (struct timeline_part) {.start = starttime};
|
||||
tl->num_parts = parts->num_parts;
|
||||
tl->track_layout = tl->parts[0].source;
|
||||
return;
|
||||
|
||||
error:
|
||||
tl->num_parts = 0;
|
||||
tl->num_chapters = 0;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
static void build_mpv_edl_timeline(struct timeline *tl)
|
||||
{
|
||||
struct priv *p = tl->demuxer->priv;
|
||||
|
||||
struct tl_parts *parts = parse_edl(p->data);
|
||||
if (!parts) {
|
||||
MP_ERR(tl, "Error in EDL.\n");
|
||||
return;
|
||||
}
|
||||
MP_TARRAY_APPEND(tl, tl->sources, tl->num_sources, tl->demuxer);
|
||||
// Source is .edl and not edl:// => don't allow arbitrary paths
|
||||
if (tl->demuxer->stream->uncached_type != STREAMTYPE_EDL)
|
||||
fix_filenames(parts, tl->demuxer->filename);
|
||||
build_timeline(tl, parts);
|
||||
talloc_free(parts);
|
||||
}
|
||||
|
||||
static int try_open_file(struct demuxer *demuxer, enum demux_check check)
|
||||
{
|
||||
struct priv *p = talloc_zero(demuxer, struct priv);
|
||||
demuxer->priv = p;
|
||||
|
||||
struct stream *s = demuxer->stream;
|
||||
if (s->uncached_type == STREAMTYPE_EDL) {
|
||||
demuxer->file_contents = bstr0(s->path);
|
||||
p->data = bstr0(s->path);
|
||||
return 0;
|
||||
}
|
||||
if (check >= DEMUX_CHECK_UNSAFE) {
|
||||
if (!bstr_equals0(stream_peek(s, strlen(HEADER)), HEADER))
|
||||
return -1;
|
||||
}
|
||||
demuxer->file_contents = stream_read_complete(s, demuxer, 1000000);
|
||||
if (demuxer->file_contents.start == NULL)
|
||||
p->data = stream_read_complete(s, demuxer, 1000000);
|
||||
if (p->data.start == NULL)
|
||||
return -1;
|
||||
bstr_eatstart0(&demuxer->file_contents, HEADER);
|
||||
bstr_eatstart0(&p->data, HEADER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const struct demuxer_desc demuxer_desc_edl = {
|
||||
.name = "edl",
|
||||
.desc = "Edit decision list",
|
||||
.type = DEMUXER_TYPE_EDL,
|
||||
.open = try_open_file,
|
||||
.load_timeline = build_mpv_edl_timeline,
|
||||
};
|
||||
+116
-92
@@ -81,7 +81,7 @@ const struct m_sub_options demux_lavf_conf = {
|
||||
OPT_INTRANGE("buffersize", buffersize, 0, 1, 10 * 1024 * 1024,
|
||||
OPTDEF_INT(BIO_BUFFER_SIZE)),
|
||||
OPT_FLAG("allow-mimetype", allow_mimetype, 0),
|
||||
OPT_INTRANGE("probescore", probescore, 0, 0, 100),
|
||||
OPT_INTRANGE("probescore", probescore, 0, 1, AVPROBE_SCORE_MAX),
|
||||
OPT_STRING("cryptokey", cryptokey, 0),
|
||||
OPT_CHOICE("genpts-mode", genptsmode, 0,
|
||||
({"lavf", 1}, {"no", 0})),
|
||||
@@ -91,12 +91,59 @@ const struct m_sub_options demux_lavf_conf = {
|
||||
.size = sizeof(struct demux_lavf_opts),
|
||||
.defaults = &(const struct demux_lavf_opts){
|
||||
.allow_mimetype = 1,
|
||||
// AVPROBE_SCORE_MAX/4 + 1 is the "recommended" limit. Below that, the
|
||||
// user is supposed to retry with larger probe sizes until a higher
|
||||
// value is reached.
|
||||
.probescore = AVPROBE_SCORE_MAX/4 + 1,
|
||||
},
|
||||
};
|
||||
|
||||
struct format_hack {
|
||||
const char *ff_name;
|
||||
const char *mime_type;
|
||||
int probescore;
|
||||
float analyzeduration;
|
||||
bool max_probe : 1; // use probescore only if max. probe size reached
|
||||
bool ignore : 1; // blacklisted
|
||||
bool no_stream : 1; // do not wrap struct stream as AVIOContext
|
||||
bool use_stream_ids : 1; // export the native stream IDs
|
||||
// Do not confuse player's position estimation (position is into external
|
||||
// segment, with e.g. HLS, player knows about the playlist main file only).
|
||||
bool clear_filepos : 1;
|
||||
};
|
||||
|
||||
#define BLACKLIST(fmt) {fmt, .ignore = true}
|
||||
|
||||
static const struct format_hack format_hacks[] = {
|
||||
// for webradios
|
||||
{"aac", "audio/aacp", 25, 0.5},
|
||||
{"aac", "audio/aac", 25, 0.5},
|
||||
|
||||
// some mp3 files don't detect correctly (usually id3v2 too large)
|
||||
{"mp3", "audio/mpeg", 24, 0.5},
|
||||
{"mp3", NULL, 24, .max_probe = true},
|
||||
|
||||
{"hls", .no_stream = true, .clear_filepos = true},
|
||||
{"mpeg", .use_stream_ids = true},
|
||||
{"mpegts", .use_stream_ids = true},
|
||||
|
||||
// Useless non-sense, sometimes breaks MLP2 subreader.c fallback
|
||||
BLACKLIST("tty"),
|
||||
// Let's open files with extremely generic extensions (.bin) with a
|
||||
// demuxer that doesn't have a probe function! NO.
|
||||
BLACKLIST("bin"),
|
||||
// Image demuxers, disabled in favor of demux_mf (for now):
|
||||
BLACKLIST("image"),
|
||||
BLACKLIST("image2pipe"),
|
||||
BLACKLIST("bmp_pipe"), BLACKLIST("dpx_pipe"), BLACKLIST("exr_pipe"),
|
||||
BLACKLIST("j2k_pipe"), BLACKLIST("png_pipe"), BLACKLIST("tiff_pipe"),
|
||||
BLACKLIST("jpeg_pipe"),
|
||||
{0}
|
||||
};
|
||||
|
||||
typedef struct lavf_priv {
|
||||
char *filename;
|
||||
const struct format_hack *format_hack;
|
||||
struct format_hack format_hack;
|
||||
AVInputFormat *avif;
|
||||
AVFormatContext *avfc;
|
||||
AVIOContext *pb;
|
||||
@@ -109,32 +156,22 @@ typedef struct lavf_priv {
|
||||
bool merge_track_metadata;
|
||||
} lavf_priv_t;
|
||||
|
||||
struct format_hack {
|
||||
const char *ff_name;
|
||||
const char *mime_type;
|
||||
int probescore;
|
||||
float analyzeduration;
|
||||
bool max_probe; // use probescore only if max. probe size reached
|
||||
};
|
||||
|
||||
static const struct format_hack format_hacks[] = {
|
||||
// for webradios
|
||||
{"aac", "audio/aacp", 25, 0.5},
|
||||
{"aac", "audio/aac", 25, 0.5},
|
||||
// some mp3 files don't detect correctly
|
||||
{"mp3", "audio/mpeg", 24, 0.5},
|
||||
{"mp3", NULL, 24, .max_probe = true},
|
||||
{0}
|
||||
};
|
||||
|
||||
static const char *const format_blacklist[] = {
|
||||
"tty", // Useless non-sense, sometimes breaks MLP2 subreader.c fallback
|
||||
// Image demuxers, disabled in favor of demux_mf:
|
||||
"image2", "image2pipe",
|
||||
"bmp_pipe", "dpx_pipe", "exr_pipe", "j2k_pipe", "png_pipe", "tiff_pipe",
|
||||
"jpeg_pipe",
|
||||
0
|
||||
};
|
||||
// At least mp4 has name="mov,mp4,m4a,3gp,3g2,mj2", so we split the name
|
||||
// on "," in general.
|
||||
static bool matches_avinputformat_name(struct lavf_priv *priv,
|
||||
const char *name)
|
||||
{
|
||||
const char *avifname = priv->avif->name;
|
||||
while (1) {
|
||||
const char *next = strchr(avifname, ',');
|
||||
if (!next)
|
||||
return !strcmp(avifname, name);
|
||||
int len = next - avifname;
|
||||
if (len == strlen(name) && !memcmp(avifname, name, len))
|
||||
return true;
|
||||
avifname = next + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int mp_read(void *opaque, uint8_t *buf, int size)
|
||||
{
|
||||
@@ -254,6 +291,7 @@ static int lavf_check_file(demuxer_t *demuxer, enum demux_check check)
|
||||
if (!lavfdopts->allow_mimetype || !mime_type)
|
||||
mime_type = "";
|
||||
|
||||
AVInputFormat *forced_format = NULL;
|
||||
const char *format = lavfdopts->format;
|
||||
if (!format)
|
||||
format = s->lavf_type;
|
||||
@@ -264,22 +302,13 @@ static int lavf_check_file(demuxer_t *demuxer, enum demux_check check)
|
||||
list_formats(demuxer);
|
||||
return -1;
|
||||
}
|
||||
priv->avif = av_find_input_format(format);
|
||||
if (!priv->avif) {
|
||||
forced_format = av_find_input_format(format);
|
||||
if (!forced_format) {
|
||||
MP_FATAL(demuxer, "Unknown lavf format %s\n", format);
|
||||
return -1;
|
||||
}
|
||||
MP_VERBOSE(demuxer, "Forced lavf %s demuxer\n", priv->avif->long_name);
|
||||
goto success;
|
||||
}
|
||||
|
||||
// AVPROBE_SCORE_MAX/4 + 1 is the "recommended" limit. Below that, the user
|
||||
// is supposed to retry with larger probe sizes until a higher value is
|
||||
// reached.
|
||||
int min_probe = AVPROBE_SCORE_MAX/4 + 1;
|
||||
if (lavfdopts->probescore)
|
||||
min_probe = lavfdopts->probescore;
|
||||
|
||||
AVProbeData avpd = {
|
||||
// Disable file-extension matching with normal checks
|
||||
.filename = check <= DEMUX_CHECK_REQUEST ? priv->filename : "",
|
||||
@@ -291,56 +320,56 @@ static int lavf_check_file(demuxer_t *demuxer, enum demux_check check)
|
||||
|
||||
bool final_probe = false;
|
||||
do {
|
||||
int nsize = av_clip(avpd.buf_size * 2, INITIAL_PROBE_SIZE,
|
||||
PROBE_BUF_SIZE);
|
||||
bstr buf = stream_peek(s, nsize);
|
||||
if (buf.len <= avpd.buf_size)
|
||||
final_probe = true;
|
||||
memcpy(avpd.buf, buf.start, buf.len);
|
||||
avpd.buf_size = buf.len;
|
||||
|
||||
int score = 0;
|
||||
priv->avif = av_probe_input_format2(&avpd, avpd.buf_size > 0, &score);
|
||||
|
||||
if (forced_format) {
|
||||
priv->avif = forced_format;
|
||||
score = AVPROBE_SCORE_MAX;
|
||||
} else {
|
||||
int nsize = av_clip(avpd.buf_size * 2, INITIAL_PROBE_SIZE,
|
||||
PROBE_BUF_SIZE);
|
||||
bstr buf = stream_peek(s, nsize);
|
||||
if (buf.len <= avpd.buf_size)
|
||||
final_probe = true;
|
||||
memcpy(avpd.buf, buf.start, buf.len);
|
||||
avpd.buf_size = buf.len;
|
||||
|
||||
priv->avif = av_probe_input_format2(&avpd, avpd.buf_size > 0, &score);
|
||||
}
|
||||
|
||||
if (priv->avif) {
|
||||
MP_VERBOSE(demuxer, "Found '%s' at score=%d size=%d.\n",
|
||||
priv->avif->name, score, avpd.buf_size);
|
||||
MP_VERBOSE(demuxer, "Found '%s' at score=%d size=%d%s.\n",
|
||||
priv->avif->name, score, avpd.buf_size,
|
||||
forced_format ? " (forced)" : "");
|
||||
|
||||
priv->format_hack = NULL;
|
||||
for (int n = 0; format_hacks[n].ff_name; n++) {
|
||||
const struct format_hack *entry = &format_hacks[n];
|
||||
if (strcmp(entry->ff_name, priv->avif->name) != 0)
|
||||
if (!matches_avinputformat_name(priv, entry->ff_name))
|
||||
continue;
|
||||
if (entry->mime_type && strcasecmp(entry->mime_type, mime_type) != 0)
|
||||
continue;
|
||||
priv->format_hack = entry;
|
||||
priv->format_hack = *entry;
|
||||
break;
|
||||
}
|
||||
|
||||
if (score >= min_probe)
|
||||
if (score >= lavfdopts->probescore)
|
||||
break;
|
||||
|
||||
if (priv->format_hack) {
|
||||
if (score >= priv->format_hack->probescore &&
|
||||
(!priv->format_hack->max_probe || final_probe))
|
||||
break;
|
||||
}
|
||||
if (priv->format_hack.probescore &&
|
||||
score >= priv->format_hack.probescore &&
|
||||
(!priv->format_hack.max_probe || final_probe))
|
||||
break;
|
||||
}
|
||||
|
||||
priv->avif = NULL;
|
||||
priv->format_hack = NULL;
|
||||
priv->format_hack = (struct format_hack){0};
|
||||
} while (!final_probe);
|
||||
|
||||
av_free(avpd.buf);
|
||||
|
||||
if (priv->avif && !format) {
|
||||
for (int n = 0; format_blacklist[n]; n++) {
|
||||
if (strcmp(format_blacklist[n], priv->avif->name) == 0) {
|
||||
MP_VERBOSE(demuxer, "Format blacklisted.\n");
|
||||
priv->avif = NULL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (priv->avif && !forced_format && priv->format_hack.ignore) {
|
||||
MP_VERBOSE(demuxer, "Format blacklisted.\n");
|
||||
priv->avif = NULL;
|
||||
}
|
||||
|
||||
if (!priv->avif) {
|
||||
@@ -348,30 +377,11 @@ static int lavf_check_file(demuxer_t *demuxer, enum demux_check check)
|
||||
return -1;
|
||||
}
|
||||
|
||||
success:
|
||||
|
||||
demuxer->filetype = priv->avif->name;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool matches_avinputformat_name(struct lavf_priv *priv,
|
||||
const char *name)
|
||||
{
|
||||
// At least mp4 has name="mov,mp4,m4a,3gp,3g2,mj2", so we split the name
|
||||
// on "," in general.
|
||||
const char *avifname = priv->avif->name;
|
||||
while (1) {
|
||||
const char *next = strchr(avifname, ',');
|
||||
if (!next)
|
||||
return !strcmp(avifname, name);
|
||||
int len = next - avifname;
|
||||
if (len == strlen(name) && !memcmp(avifname, name, len))
|
||||
return true;
|
||||
avifname = next + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t char2int(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
@@ -597,8 +607,7 @@ static void handle_stream(demuxer_t *demuxer, int i)
|
||||
|
||||
if (st->disposition & AV_DISPOSITION_DEFAULT)
|
||||
sh->default_track = 1;
|
||||
if (matches_avinputformat_name(priv, "mpeg") ||
|
||||
matches_avinputformat_name(priv, "mpegts"))
|
||||
if (priv->format_hack.use_stream_ids)
|
||||
sh->demuxer_id = st->id;
|
||||
AVDictionaryEntry *title = av_dict_get(st->metadata, "title", NULL, 0);
|
||||
if (title && title->value)
|
||||
@@ -662,6 +671,12 @@ static void update_metadata(demuxer_t *demuxer, AVPacket *pkt)
|
||||
#endif
|
||||
}
|
||||
|
||||
static int interrupt_cb(void *ctx)
|
||||
{
|
||||
struct demuxer *demuxer = ctx;
|
||||
return mp_cancel_test(demuxer->stream->cancel);
|
||||
}
|
||||
|
||||
static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check)
|
||||
{
|
||||
struct MPOpts *opts = demuxer->opts;
|
||||
@@ -702,8 +717,8 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check)
|
||||
lavfdopts->probesize);
|
||||
}
|
||||
|
||||
if (priv->format_hack && priv->format_hack->analyzeduration)
|
||||
analyze_duration = priv->format_hack->analyzeduration;
|
||||
if (priv->format_hack.analyzeduration)
|
||||
analyze_duration = priv->format_hack.analyzeduration;
|
||||
if (lavfdopts->analyzeduration)
|
||||
analyze_duration = lavfdopts->analyzeduration;
|
||||
if (analyze_duration > 0) {
|
||||
@@ -717,7 +732,7 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check)
|
||||
|
||||
if ((priv->avif->flags & AVFMT_NOFILE) ||
|
||||
demuxer->stream->type == STREAMTYPE_AVDEVICE ||
|
||||
matches_avinputformat_name(priv, "hls"))
|
||||
priv->format_hack.no_stream)
|
||||
{
|
||||
mp_setup_av_network_options(&dopts, demuxer->global, demuxer->log, opts);
|
||||
// This might be incorrect.
|
||||
@@ -750,6 +765,11 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check)
|
||||
av_dict_set(&dopts, "rtsp_transport", transport, 0);
|
||||
}
|
||||
|
||||
avfc->interrupt_callback = (AVIOInterruptCB){
|
||||
.callback = interrupt_cb,
|
||||
.opaque = demuxer,
|
||||
};
|
||||
|
||||
mp_set_avdict(&dopts, lavfdopts->avopts);
|
||||
|
||||
if (avformat_open_input(&avfc, priv->filename, priv->avif, &dopts) < 0) {
|
||||
@@ -856,6 +876,10 @@ static int demux_lavf_fill_buffer(demuxer_t *demux)
|
||||
priv->last_pts = dp->dts * AV_TIME_BASE;
|
||||
}
|
||||
av_free_packet(pkt);
|
||||
|
||||
if (priv->format_hack.clear_filepos)
|
||||
dp->pos = -1;
|
||||
|
||||
demux_add_packet(stream, dp);
|
||||
return 1;
|
||||
}
|
||||
|
||||
+58
-63
@@ -168,7 +168,6 @@ typedef struct mkv_demuxer {
|
||||
mkv_index_t *indexes;
|
||||
size_t num_indexes;
|
||||
bool index_complete;
|
||||
uint64_t deferred_cues;
|
||||
|
||||
struct header_elem {
|
||||
int32_t id;
|
||||
@@ -183,6 +182,8 @@ typedef struct mkv_demuxer {
|
||||
int subtitle_preroll;
|
||||
|
||||
bool index_has_durations;
|
||||
|
||||
bool eof_warning;
|
||||
} mkv_demuxer_t;
|
||||
|
||||
#define REALHEADER_SIZE 16
|
||||
@@ -704,8 +705,6 @@ static int demux_mkv_read_cues(demuxer_t *demuxer)
|
||||
mkv_demuxer_t *mkv_d = (mkv_demuxer_t *) demuxer->priv;
|
||||
stream_t *s = demuxer->stream;
|
||||
|
||||
mkv_d->deferred_cues = 0;
|
||||
|
||||
if (opts->index_mode != 1) {
|
||||
ebml_read_skip(demuxer->log, -1, s);
|
||||
return 0;
|
||||
@@ -750,26 +749,6 @@ static int demux_mkv_read_cues(demuxer_t *demuxer)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void read_deferred_cues(demuxer_t *demuxer)
|
||||
{
|
||||
mkv_demuxer_t *mkv_d = demuxer->priv;
|
||||
stream_t *s = demuxer->stream;
|
||||
|
||||
if (mkv_d->deferred_cues) {
|
||||
int64_t pos = mkv_d->deferred_cues;
|
||||
mkv_d->deferred_cues = 0;
|
||||
if (!stream_seek(s, pos)) {
|
||||
MP_WARN(demuxer, "Failed to seek to cues\n");
|
||||
return;
|
||||
}
|
||||
if (ebml_read_id(s) != MATROSKA_ID_CUES) {
|
||||
MP_WARN(demuxer, "Expected element not found\n");
|
||||
return;
|
||||
}
|
||||
demux_mkv_read_cues(demuxer);
|
||||
}
|
||||
}
|
||||
|
||||
static int demux_mkv_read_chapters(struct demuxer *demuxer)
|
||||
{
|
||||
struct MPOpts *opts = demuxer->opts;
|
||||
@@ -1009,9 +988,6 @@ static int demux_mkv_read_attachments(demuxer_t *demuxer)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_header_element(struct demuxer *demuxer, uint32_t id,
|
||||
int64_t at_filepos);
|
||||
|
||||
static struct header_elem *get_header_element(struct demuxer *demuxer,
|
||||
uint32_t id,
|
||||
int64_t element_filepos)
|
||||
@@ -1060,6 +1036,9 @@ static int demux_mkv_read_seekhead(demuxer_t *demuxer)
|
||||
struct ebml_seek_head seekhead = {0};
|
||||
struct ebml_parse_ctx parse_ctx = {demuxer->log};
|
||||
|
||||
int64_t end = 0;
|
||||
stream_control(s, STREAM_CTRL_GET_SIZE, &end);
|
||||
|
||||
MP_VERBOSE(demuxer, "/---- [ parsing seek head ] ---------\n");
|
||||
if (ebml_read_element(s, &parse_ctx, &seekhead, &ebml_seek_head_desc) < 0) {
|
||||
res = -1;
|
||||
@@ -1074,13 +1053,16 @@ static int demux_mkv_read_seekhead(demuxer_t *demuxer)
|
||||
uint64_t pos = seek->seek_position + mkv_d->segment_start;
|
||||
MP_DBG(demuxer, "Element 0x%x at %"PRIu64".\n",
|
||||
(unsigned)seek->seek_id, pos);
|
||||
get_header_element(demuxer, seek->seek_id, pos);
|
||||
// This is nice to warn against incomplete files.
|
||||
int64_t end = 0;
|
||||
stream_control(s, STREAM_CTRL_GET_SIZE, &end);
|
||||
if (pos >= end)
|
||||
MP_WARN(demuxer, "SeekHead position beyond "
|
||||
"end of file - incomplete file?\n");
|
||||
struct header_elem *elem = get_header_element(demuxer, seek->seek_id, pos);
|
||||
// Warn against incomplete files.
|
||||
if (elem && pos >= end) {
|
||||
elem->parsed = true; // don't bother
|
||||
if (!mkv_d->eof_warning) {
|
||||
MP_WARN(demuxer, "SeekHead position beyond "
|
||||
"end of file - incomplete file?\n");
|
||||
mkv_d->eof_warning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
out:
|
||||
MP_VERBOSE(demuxer, "\\---- [ parsing seek head ] ---------\n");
|
||||
@@ -1118,6 +1100,45 @@ skip:
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_deferred_element(struct demuxer *demuxer,
|
||||
struct header_elem *elem)
|
||||
{
|
||||
stream_t *s = demuxer->stream;
|
||||
|
||||
if (elem->parsed)
|
||||
return 0;
|
||||
elem->parsed = true;
|
||||
MP_VERBOSE(demuxer, "Seeking to %"PRIu64" to read header element 0x%x.\n",
|
||||
elem->pos, (unsigned)elem->id);
|
||||
if (!stream_seek(s, elem->pos)) {
|
||||
MP_WARN(demuxer, "Failed to seek when reading header element.\n");
|
||||
return 0;
|
||||
}
|
||||
if (ebml_read_id(s) != elem->id) {
|
||||
MP_ERR(demuxer, "Expected element 0x%x not found\n",
|
||||
(unsigned int)elem->id);
|
||||
return 0;
|
||||
}
|
||||
elem->parsed = false; // don't make read_header_element skip it
|
||||
return read_header_element(demuxer, elem->id, elem->pos);
|
||||
}
|
||||
|
||||
static void read_deferred_cues(demuxer_t *demuxer)
|
||||
{
|
||||
struct MPOpts *opts = demuxer->opts;
|
||||
mkv_demuxer_t *mkv_d = demuxer->priv;
|
||||
|
||||
if (mkv_d->index_complete || opts->index_mode != 1)
|
||||
return;
|
||||
|
||||
for (int n = 0; n < mkv_d->num_headers; n++) {
|
||||
struct header_elem *elem = &mkv_d->headers[n];
|
||||
|
||||
if (elem->id == MATROSKA_ID_CUES)
|
||||
read_deferred_element(demuxer, elem);
|
||||
}
|
||||
}
|
||||
|
||||
static void add_coverart(struct demuxer *demuxer)
|
||||
{
|
||||
for (int n = 0; n < demuxer->num_attachments; n++) {
|
||||
@@ -1283,8 +1304,6 @@ static int demux_mkv_open_video(demuxer_t *demuxer, mkv_track_t *track)
|
||||
return 1;
|
||||
}
|
||||
|
||||
sh_v->coded_width = track->v_width;
|
||||
sh_v->coded_height = track->v_height;
|
||||
sh_v->extradata = talloc_memdup(sh_v, extradata, extradata_size);
|
||||
sh_v->extradata_len = extradata_size;
|
||||
if (!sh->codec) {
|
||||
@@ -1796,36 +1815,12 @@ static int demux_mkv_open(demuxer_t *demuxer, enum demux_check check)
|
||||
// Likewise, ->headers might be reallocated.
|
||||
for (int n = 0; n < mkv_d->num_headers; n++) {
|
||||
struct header_elem *elem = &mkv_d->headers[n];
|
||||
if (elem->parsed)
|
||||
continue;
|
||||
elem->parsed = true;
|
||||
if (elem->id == MATROSKA_ID_CUES) {
|
||||
if (elem->id == MATROSKA_ID_CUES && !elem->parsed) {
|
||||
// Read cues when they are needed, to avoid seeking on opening.
|
||||
MP_VERBOSE(demuxer, "Deferring reading cues.\n");
|
||||
mkv_d->deferred_cues = elem->pos;
|
||||
continue;
|
||||
}
|
||||
MP_VERBOSE(demuxer, "Seeking to %"PRIu64" to read header element 0x%x.\n",
|
||||
elem->pos, (unsigned)elem->id);
|
||||
int64_t end = 0;
|
||||
stream_control(s, STREAM_CTRL_GET_SIZE, &end);
|
||||
if (elem->pos >= end) {
|
||||
MP_WARN(demuxer, "SeekHead position beyond "
|
||||
"end of file - incomplete file?\n");
|
||||
continue;
|
||||
}
|
||||
if (!stream_seek(s, elem->pos)) {
|
||||
MP_WARN(demuxer, "Failed to seek when reading header element.\n");
|
||||
continue;
|
||||
}
|
||||
if (ebml_read_id(s) != elem->id) {
|
||||
MP_ERR(demuxer, "Expected element 0x%x not found\n",
|
||||
(unsigned int)elem->id);
|
||||
continue;
|
||||
}
|
||||
elem->parsed = false; // don't make read_header_element skip it
|
||||
int res = read_header_element(demuxer, elem->id, elem->pos);
|
||||
if (res < 0)
|
||||
if (read_deferred_element(demuxer, elem) < 0)
|
||||
return -1;
|
||||
}
|
||||
if (!stream_seek(s, start_pos)) {
|
||||
@@ -2882,12 +2877,12 @@ static void mkv_free(struct demuxer *demuxer)
|
||||
const demuxer_desc_t demuxer_desc_matroska = {
|
||||
.name = "mkv",
|
||||
.desc = "Matroska",
|
||||
.type = DEMUXER_TYPE_MATROSKA,
|
||||
.open = demux_mkv_open,
|
||||
.fill_buffer = demux_mkv_fill_buffer,
|
||||
.close = mkv_free,
|
||||
.seek = demux_mkv_seek,
|
||||
.control = demux_mkv_control
|
||||
.control = demux_mkv_control,
|
||||
.load_timeline = build_ordered_chapter_timeline,
|
||||
};
|
||||
|
||||
bool demux_matroska_uid_cmp(struct matroska_segment_uid *a,
|
||||
|
||||
@@ -32,15 +32,37 @@
|
||||
|
||||
#include "talloc.h"
|
||||
|
||||
#include "player/core.h"
|
||||
#include "common/msg.h"
|
||||
#include "common/global.h"
|
||||
#include "demux/demux.h"
|
||||
#include "demux/timeline.h"
|
||||
#include "demux/matroska.h"
|
||||
#include "options/options.h"
|
||||
#include "options/path.h"
|
||||
#include "misc/bstr.h"
|
||||
#include "common/common.h"
|
||||
#include "common/playlist.h"
|
||||
#include "stream/stream.h"
|
||||
|
||||
struct tl_ctx {
|
||||
struct mp_log *log;
|
||||
struct mpv_global *global;
|
||||
|
||||
struct demuxer *demuxer;
|
||||
|
||||
struct demuxer **sources;
|
||||
int num_sources;
|
||||
|
||||
struct timeline_part *timeline;
|
||||
int num_parts;
|
||||
|
||||
struct matroska_segment_uid *uids;
|
||||
uint64_t start_time; // When the next part should start on the complete timeline.
|
||||
uint64_t missing_time; // Total missing time so far.
|
||||
uint64_t last_end_time; // When the last part ended on the complete timeline.
|
||||
int num_chapters; // Total number of expected chapters.
|
||||
};
|
||||
|
||||
struct find_entry {
|
||||
char *name;
|
||||
int matchlen;
|
||||
@@ -127,10 +149,10 @@ static char **find_files(const char *original_file)
|
||||
return results;
|
||||
}
|
||||
|
||||
static int enable_cache(struct MPContext *mpctx, struct stream **stream,
|
||||
static int enable_cache(struct mpv_global *global, struct stream **stream,
|
||||
struct demuxer **demuxer, struct demuxer_params *params)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
struct MPOpts *opts = global->opts;
|
||||
|
||||
if (!stream_wants_cache(*stream, &opts->stream_cache))
|
||||
return 0;
|
||||
@@ -139,7 +161,7 @@ static int enable_cache(struct MPContext *mpctx, struct stream **stream,
|
||||
free_demuxer(*demuxer);
|
||||
free_stream(*stream);
|
||||
|
||||
*stream = stream_open(filename, mpctx->global);
|
||||
*stream = stream_open(filename, global);
|
||||
if (!*stream) {
|
||||
talloc_free(filename);
|
||||
return -1;
|
||||
@@ -147,7 +169,7 @@ static int enable_cache(struct MPContext *mpctx, struct stream **stream,
|
||||
|
||||
stream_enable_cache(stream, &opts->stream_cache);
|
||||
|
||||
*demuxer = demux_open(*stream, "mkv", params, mpctx->global);
|
||||
*demuxer = demux_open(*stream, "mkv", params, global);
|
||||
if (!*demuxer) {
|
||||
talloc_free(filename);
|
||||
free_stream(*stream);
|
||||
@@ -158,185 +180,161 @@ static int enable_cache(struct MPContext *mpctx, struct stream **stream,
|
||||
return 1;
|
||||
}
|
||||
|
||||
static bool has_source_request(struct matroska_segment_uid *uids,
|
||||
int num_sources,
|
||||
static bool has_source_request(struct tl_ctx *ctx,
|
||||
struct matroska_segment_uid *new_uid)
|
||||
{
|
||||
for (int i = 0; i < num_sources; ++i) {
|
||||
if (demux_matroska_uid_cmp(uids + i, new_uid))
|
||||
for (int i = 0; i < ctx->num_sources; ++i) {
|
||||
if (demux_matroska_uid_cmp(&ctx->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)
|
||||
static bool check_file_seg(struct tl_ctx *ctx, char *filename, int segment)
|
||||
{
|
||||
bool was_valid = false;
|
||||
struct demuxer_params params = {
|
||||
.matroska_num_wanted_uids = *num_sources,
|
||||
.matroska_wanted_uids = *uids,
|
||||
.matroska_num_wanted_uids = ctx->num_sources,
|
||||
.matroska_wanted_uids = ctx->uids,
|
||||
.matroska_wanted_segment = segment,
|
||||
.matroska_was_valid = &was_valid,
|
||||
};
|
||||
struct stream *s = stream_open(filename, mpctx->global);
|
||||
struct stream *s = stream_open(filename, ctx->global);
|
||||
if (!s)
|
||||
return false;
|
||||
struct demuxer *d = demux_open(s, "mkv", ¶ms, mpctx->global);
|
||||
struct demuxer *d = demux_open(s, "mkv", ¶ms, ctx->global);
|
||||
|
||||
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_INFO(mpctx, "Match for source %d: %s\n", i, d->filename);
|
||||
struct matroska_data *m = &d->matroska_data;
|
||||
|
||||
for (int j = 0; j < m->num_ordered_chapters; j++) {
|
||||
struct matroska_chapter *c = m->ordered_chapters + j;
|
||||
for (int i = 1; i < ctx->num_sources; i++) {
|
||||
struct matroska_segment_uid *uid = &ctx->uids[i];
|
||||
if (ctx->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_INFO(ctx, "Match for source %d: %s\n", i, d->filename);
|
||||
|
||||
if (!c->has_segment_uid)
|
||||
continue;
|
||||
for (int j = 0; j < m->num_ordered_chapters; j++) {
|
||||
struct matroska_chapter *c = m->ordered_chapters + j;
|
||||
|
||||
if (has_source_request(*uids, *num_sources, &c->uid))
|
||||
continue;
|
||||
|
||||
/* Set the requested segment. */
|
||||
MP_TARRAY_GROW(NULL, *uids, *num_sources);
|
||||
(*uids)[*num_sources] = c->uid;
|
||||
|
||||
/* Add a new source slot. */
|
||||
MP_TARRAY_APPEND(NULL, *sources, *num_sources, NULL);
|
||||
}
|
||||
|
||||
if (enable_cache(mpctx, &s, &d, ¶ms) < 0)
|
||||
if (!c->has_segment_uid)
|
||||
continue;
|
||||
|
||||
(*sources)[i] = d;
|
||||
return true;
|
||||
if (has_source_request(ctx, &c->uid))
|
||||
continue;
|
||||
|
||||
/* Set the requested segment. */
|
||||
MP_TARRAY_GROW(NULL, ctx->uids, ctx->num_sources);
|
||||
ctx->uids[ctx->num_sources] = c->uid;
|
||||
|
||||
/* Add a new source slot. */
|
||||
MP_TARRAY_APPEND(NULL, ctx->sources, ctx->num_sources, NULL);
|
||||
}
|
||||
|
||||
params.matroska_wanted_uids = ctx->uids; // potentially reallocated, same data
|
||||
if (enable_cache(ctx->global, &s, &d, ¶ms) < 0)
|
||||
continue;
|
||||
|
||||
ctx->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)
|
||||
static void check_file(struct tl_ctx *ctx, char *filename, int first)
|
||||
{
|
||||
for (int segment = first; ; segment++) {
|
||||
if (!check_file_seg(mpctx, sources, num_sources, uids, filename, segment))
|
||||
if (!check_file_seg(ctx, filename, segment))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool missing(struct demuxer **sources, int num_sources)
|
||||
static bool missing(struct tl_ctx *ctx)
|
||||
{
|
||||
for (int i = 0; i < num_sources; i++) {
|
||||
if (!sources[i])
|
||||
for (int i = 0; i < ctx->num_sources; i++) {
|
||||
if (!ctx->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)
|
||||
static void find_ordered_chapter_sources(struct tl_ctx *ctx)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
struct MPOpts *opts = ctx->global->opts;
|
||||
void *tmp = talloc_new(NULL);
|
||||
int num_filenames = 0;
|
||||
char **filenames = NULL;
|
||||
if (*num_sources > 1) {
|
||||
char *main_filename = mpctx->demuxer->filename;
|
||||
MP_INFO(mpctx, "This file references data from other sources.\n");
|
||||
if (ctx->num_sources > 1) {
|
||||
char *main_filename = ctx->demuxer->filename;
|
||||
MP_INFO(ctx, "This file references data from other sources.\n");
|
||||
if (opts->ordered_chapters_files && opts->ordered_chapters_files[0]) {
|
||||
MP_INFO(mpctx, "Loading references from '%s'.\n",
|
||||
MP_INFO(ctx, "Loading references from '%s'.\n",
|
||||
opts->ordered_chapters_files);
|
||||
struct playlist *pl =
|
||||
playlist_parse_file(opts->ordered_chapters_files, mpctx->global);
|
||||
playlist_parse_file(opts->ordered_chapters_files, ctx->global);
|
||||
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_WARN(mpctx, "Playback source is not a "
|
||||
} else if (ctx->demuxer->stream->uncached_type != STREAMTYPE_FILE) {
|
||||
MP_WARN(ctx, "Playback source is not a "
|
||||
"normal disk file. Will not search for related files.\n");
|
||||
} else {
|
||||
MP_INFO(mpctx, "Will scan other files in the "
|
||||
MP_INFO(ctx, "Will scan other files in the "
|
||||
"same directory to find referenced sources.\n");
|
||||
filenames = find_files(main_filename);
|
||||
num_filenames = MP_TALLOC_AVAIL(filenames);
|
||||
talloc_steal(tmp, filenames);
|
||||
}
|
||||
// Possibly get further segments appended to the first segment
|
||||
check_file(mpctx, sources, num_sources, uids, main_filename, 1);
|
||||
check_file(ctx, main_filename, 1);
|
||||
}
|
||||
|
||||
int old_source_count;
|
||||
do {
|
||||
old_source_count = *num_sources;
|
||||
old_source_count = ctx->num_sources;
|
||||
for (int i = 0; i < num_filenames; i++) {
|
||||
if (!missing(*sources, *num_sources))
|
||||
if (!missing(ctx))
|
||||
break;
|
||||
MP_VERBOSE(mpctx, "Checking file %s\n", filenames[i]);
|
||||
check_file(mpctx, sources, num_sources, uids, filenames[i], 0);
|
||||
MP_VERBOSE(ctx, "Checking file %s\n", filenames[i]);
|
||||
check_file(ctx, filenames[i], 0);
|
||||
}
|
||||
} while (old_source_count != *num_sources);
|
||||
} while (old_source_count != ctx->num_sources);
|
||||
|
||||
if (missing(*sources, *num_sources)) {
|
||||
MP_ERR(mpctx, "Failed to find ordered chapter part!\n");
|
||||
if (missing(ctx)) {
|
||||
MP_ERR(ctx, "Failed to find ordered chapter part!\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));
|
||||
for (int i = 1; i < ctx->num_sources; i++) {
|
||||
if (ctx->sources[i]) {
|
||||
ctx->sources[j] = ctx->sources[i];
|
||||
ctx->uids[j] = ctx->uids[i];
|
||||
j++;
|
||||
}
|
||||
}
|
||||
*num_sources = j;
|
||||
ctx->num_sources = j;
|
||||
}
|
||||
|
||||
talloc_free(tmp);
|
||||
return *num_sources;
|
||||
}
|
||||
|
||||
struct timeline_info {
|
||||
struct demuxer **sources;
|
||||
int num_sources;
|
||||
struct timeline_part **timeline;
|
||||
int num_parts;
|
||||
|
||||
uint64_t start_time; // When the next part should start on the complete timeline.
|
||||
uint64_t missing_time; // Total missing time so far.
|
||||
uint64_t last_end_time; // When the last part ended on the complete timeline.
|
||||
int num_chapters; // Total number of expected chapters.
|
||||
};
|
||||
|
||||
struct inner_timeline_info {
|
||||
uint64_t skip; // Amount of time to skip.
|
||||
uint64_t limit; // How much time is expected for the parent chapter.
|
||||
};
|
||||
|
||||
static int64_t add_timeline_part(struct MPContext *mpctx,
|
||||
struct timeline_info *ctx,
|
||||
static int64_t add_timeline_part(struct tl_ctx *ctx,
|
||||
struct demuxer *source,
|
||||
uint64_t start)
|
||||
{
|
||||
@@ -345,18 +343,18 @@ static int64_t add_timeline_part(struct MPContext *mpctx,
|
||||
* early; we don't want to try seeking over a one frame gap. */
|
||||
int64_t join_diff = start - ctx->last_end_time;
|
||||
if (ctx->num_parts == 0
|
||||
|| FFABS(join_diff) > mpctx->opts->chapter_merge_threshold * 1e6
|
||||
|| source != (*ctx->timeline)[ctx->num_parts - 1].source)
|
||||
|| FFABS(join_diff) > ctx->global->opts->chapter_merge_threshold * 1e6
|
||||
|| source != ctx->timeline[ctx->num_parts - 1].source)
|
||||
{
|
||||
struct timeline_part new = {
|
||||
.start = ctx->start_time / 1e9,
|
||||
.source_start = start / 1e9,
|
||||
.source = source,
|
||||
};
|
||||
MP_TARRAY_APPEND(NULL, *ctx->timeline, ctx->num_parts, new);
|
||||
MP_TARRAY_APPEND(NULL, ctx->timeline, ctx->num_parts, new);
|
||||
} else if (ctx->num_parts > 0 && join_diff) {
|
||||
// Chapter was merged at an inexact boundary; adjust timestamps to match.
|
||||
MP_VERBOSE(mpctx, "Merging timeline part %d with offset %g ms.\n",
|
||||
MP_VERBOSE(ctx, "Merging timeline part %d with offset %g ms.\n",
|
||||
ctx->num_parts, join_diff / 1e6);
|
||||
ctx->start_time += join_diff;
|
||||
return join_diff;
|
||||
@@ -365,9 +363,8 @@ static int64_t add_timeline_part(struct MPContext *mpctx,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void build_timeline_loop(struct MPContext *mpctx,
|
||||
static void build_timeline_loop(struct tl_ctx *ctx,
|
||||
struct demux_chapter *chapters,
|
||||
struct timeline_info *ctx,
|
||||
struct inner_timeline_info *info,
|
||||
int current_source)
|
||||
{
|
||||
@@ -431,8 +428,7 @@ static void build_timeline_loop(struct MPContext *mpctx,
|
||||
chapter_length = source_length;
|
||||
}
|
||||
|
||||
join_diff = add_timeline_part(mpctx, ctx,
|
||||
linked_source, c->start);
|
||||
join_diff = add_timeline_part(ctx, linked_source, c->start);
|
||||
|
||||
/* 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
|
||||
@@ -453,7 +449,7 @@ static void build_timeline_loop(struct MPContext *mpctx,
|
||||
.skip = c->start,
|
||||
.limit = c->end
|
||||
};
|
||||
build_timeline_loop(mpctx, chapters, ctx, &new_info, j);
|
||||
build_timeline_loop(ctx, chapters, &new_info, j);
|
||||
// Already handled by the loop call.
|
||||
chapter_length = 0;
|
||||
}
|
||||
@@ -478,12 +474,12 @@ static void build_timeline_loop(struct MPContext *mpctx,
|
||||
ctx->missing_time += info->limit - local_starttime;
|
||||
}
|
||||
|
||||
static void check_track_compatibility(struct MPContext *mpctx)
|
||||
static void check_track_compatibility(struct timeline *tl)
|
||||
{
|
||||
struct demuxer *mainsrc = mpctx->track_layout;
|
||||
struct demuxer *mainsrc = tl->track_layout;
|
||||
|
||||
for (int n = 0; n < mpctx->num_timeline_parts; n++) {
|
||||
struct timeline_part *p = &mpctx->timeline[n];
|
||||
for (int n = 0; n < tl->num_parts; n++) {
|
||||
struct timeline_part *p = &tl->parts[n];
|
||||
if (p->source == mainsrc)
|
||||
continue;
|
||||
|
||||
@@ -493,12 +489,12 @@ static void check_track_compatibility(struct MPContext *mpctx)
|
||||
continue;
|
||||
|
||||
if (!demuxer_stream_by_demuxer_id(mainsrc, s->type, s->demuxer_id)) {
|
||||
MP_WARN(mpctx, "Source %s has %s stream with TID=%d, which "
|
||||
"is not present in the ordered chapters main "
|
||||
"file. This is a broken file. "
|
||||
"The additional stream is ignored.\n",
|
||||
p->source->filename, stream_type_name(s->type),
|
||||
s->demuxer_id);
|
||||
MP_WARN(tl, "Source %s has %s stream with TID=%d, which "
|
||||
"is not present in the ordered chapters main "
|
||||
"file. This is a broken file. "
|
||||
"The additional stream is ignored.\n",
|
||||
p->source->filename, stream_type_name(s->type),
|
||||
s->demuxer_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,45 +509,53 @@ static void check_track_compatibility(struct MPContext *mpctx)
|
||||
// There are actually many more things that in theory have to
|
||||
// match (though mpv's implementation doesn't care).
|
||||
if (s->codec && m->codec && strcmp(s->codec, m->codec) != 0)
|
||||
MP_WARN(mpctx, "Timeline segments have mismatching codec.\n");
|
||||
MP_WARN(tl, "Timeline segments have mismatching codec.\n");
|
||||
} else {
|
||||
MP_WARN(mpctx, "Source %s lacks %s stream with TID=%d, which "
|
||||
"is present in the ordered chapters main "
|
||||
"file. This is a broken file.\n",
|
||||
p->source->filename, stream_type_name(m->type),
|
||||
m->demuxer_id);
|
||||
MP_WARN(tl, "Source %s lacks %s stream with TID=%d, which "
|
||||
"is present in the ordered chapters main "
|
||||
"file. This is a broken file.\n",
|
||||
p->source->filename, stream_type_name(m->type),
|
||||
m->demuxer_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void build_ordered_chapter_timeline(struct MPContext *mpctx)
|
||||
void build_ordered_chapter_timeline(struct timeline *tl)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
struct demuxer *demuxer = tl->demuxer;
|
||||
|
||||
if (!opts->ordered_chapters) {
|
||||
MP_INFO(mpctx, "File uses ordered chapters, but "
|
||||
"you have disabled support for them. Ignoring.\n");
|
||||
if (!demuxer->matroska_data.ordered_chapters)
|
||||
return;
|
||||
|
||||
if (!demuxer->global->opts->ordered_chapters) {
|
||||
MP_INFO(demuxer, "File uses ordered chapters, but "
|
||||
"you have disabled support for them. Ignoring.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
MP_INFO(mpctx, "File uses ordered chapters, will build "
|
||||
"edit timeline.\n");
|
||||
struct tl_ctx *ctx = talloc_ptrtype(tl, ctx);
|
||||
*ctx = (struct tl_ctx){
|
||||
.log = tl->log,
|
||||
.global = tl->global,
|
||||
.demuxer = demuxer,
|
||||
};
|
||||
|
||||
MP_INFO(ctx, "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;
|
||||
ctx->sources = talloc_zero_array(tl, struct demuxer *,
|
||||
m->num_ordered_chapters + 1);
|
||||
ctx->sources[0] = demuxer;
|
||||
ctx->num_sources = 1;
|
||||
|
||||
ctx->uids = talloc_zero_array(NULL, struct matroska_segment_uid,
|
||||
m->num_ordered_chapters + 1);
|
||||
ctx->uids[0] = m->uid;
|
||||
ctx->uids[0].edition = 0;
|
||||
|
||||
for (int i = 0; i < m->num_ordered_chapters; i++) {
|
||||
struct matroska_chapter *c = m->ordered_chapters + i;
|
||||
@@ -562,39 +566,33 @@ void build_ordered_chapter_timeline(struct MPContext *mpctx)
|
||||
if (!c->has_segment_uid || demux_matroska_uid_cmp(&c->uid, &m->uid))
|
||||
continue;
|
||||
|
||||
if (has_source_request(uids, num_sources, &c->uid))
|
||||
if (has_source_request(ctx, &c->uid))
|
||||
continue;
|
||||
|
||||
memcpy(uids + num_sources, &c->uid, sizeof(c->uid));
|
||||
sources[num_sources] = NULL;
|
||||
num_sources++;
|
||||
ctx->uids[ctx->num_sources] = c->uid;
|
||||
ctx->sources[ctx->num_sources] = NULL;
|
||||
ctx->num_sources++;
|
||||
}
|
||||
|
||||
num_sources = find_ordered_chapter_sources(mpctx, &sources, &num_sources,
|
||||
&uids);
|
||||
talloc_free(uids);
|
||||
find_ordered_chapter_sources(ctx);
|
||||
|
||||
talloc_free(ctx->uids);
|
||||
ctx->uids = NULL;
|
||||
|
||||
struct timeline_part *timeline = talloc_array_ptrtype(NULL, timeline, 0);
|
||||
struct demux_chapter *chapters =
|
||||
talloc_zero_array(NULL, struct demux_chapter, m->num_ordered_chapters);
|
||||
talloc_zero_array(tl, struct demux_chapter, m->num_ordered_chapters);
|
||||
// Stupid hack, because fuck everything.
|
||||
for (int n = 0; n < m->num_ordered_chapters; n++)
|
||||
chapters[n].pts = -1;
|
||||
struct timeline_info ctx = {
|
||||
.sources = sources,
|
||||
.num_sources = num_sources,
|
||||
.timeline = &timeline,
|
||||
.num_parts = 0,
|
||||
.start_time = 0,
|
||||
.missing_time = 0,
|
||||
.last_end_time = 0,
|
||||
.num_chapters = m->num_ordered_chapters
|
||||
};
|
||||
|
||||
ctx->timeline = talloc_array_ptrtype(tl, ctx->timeline, 0);
|
||||
ctx->num_chapters = m->num_ordered_chapters;
|
||||
|
||||
struct inner_timeline_info info = {
|
||||
.skip = 0,
|
||||
.limit = 0
|
||||
};
|
||||
build_timeline_loop(mpctx, chapters, &ctx, &info, 0);
|
||||
build_timeline_loop(ctx, chapters, &info, 0);
|
||||
|
||||
// Fuck everything (2): filter out all "unset" chapters.
|
||||
for (int n = m->num_ordered_chapters - 1; n >= 0; n--) {
|
||||
@@ -602,45 +600,45 @@ void build_ordered_chapter_timeline(struct MPContext *mpctx)
|
||||
MP_TARRAY_REMOVE_AT(chapters, m->num_ordered_chapters, n);
|
||||
}
|
||||
|
||||
if (!ctx.num_parts) {
|
||||
if (!ctx->num_parts) {
|
||||
// None of the parts come from the file itself???
|
||||
// Broken file, but we need at least 1 valid timeline part - add a dummy.
|
||||
MP_WARN(mpctx, "Ordered chapters file with no parts?\n");
|
||||
MP_WARN(ctx, "Ordered chapters file with no parts?\n");
|
||||
struct timeline_part new = {
|
||||
.source = demuxer,
|
||||
};
|
||||
MP_TARRAY_APPEND(NULL, timeline, ctx.num_parts, new);
|
||||
MP_TARRAY_APPEND(NULL, ctx->timeline, ctx->num_parts, new);
|
||||
}
|
||||
|
||||
struct timeline_part new = {
|
||||
.start = ctx.start_time / 1e9,
|
||||
.start = ctx->start_time / 1e9,
|
||||
};
|
||||
MP_TARRAY_APPEND(NULL, timeline, ctx.num_parts, new);
|
||||
MP_TARRAY_APPEND(NULL, ctx->timeline, ctx->num_parts, 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 (ctx.missing_time >= 1e6) {
|
||||
MP_ERR(mpctx, "There are %.3f seconds missing from the timeline!\n",
|
||||
ctx.missing_time / 1e9);
|
||||
if (ctx->missing_time >= 1e6) {
|
||||
MP_ERR(ctx, "There are %.3f seconds missing from the timeline!\n",
|
||||
ctx->missing_time / 1e9);
|
||||
}
|
||||
talloc_free(mpctx->sources);
|
||||
mpctx->sources = sources;
|
||||
mpctx->num_sources = num_sources;
|
||||
mpctx->timeline = timeline;
|
||||
mpctx->num_timeline_parts = ctx.num_parts - 1;
|
||||
mpctx->num_chapters = m->num_ordered_chapters;
|
||||
mpctx->chapters = chapters;
|
||||
|
||||
tl->sources = ctx->sources;
|
||||
tl->num_sources = ctx->num_sources;
|
||||
tl->parts = ctx->timeline;
|
||||
tl->num_parts = ctx->num_parts - 1; // minus termination
|
||||
tl->chapters = chapters;
|
||||
tl->num_chapters = m->num_ordered_chapters;
|
||||
|
||||
// With Matroska, the "master" file usually dictates track layout etc.,
|
||||
// except maybe with playlist-like files.
|
||||
mpctx->track_layout = mpctx->timeline[0].source;
|
||||
for (int n = 0; n < mpctx->num_timeline_parts; n++) {
|
||||
if (mpctx->timeline[n].source == mpctx->demuxer) {
|
||||
mpctx->track_layout = mpctx->demuxer;
|
||||
tl->track_layout = tl->parts[0].source;
|
||||
for (int n = 0; n < tl->num_parts; n++) {
|
||||
if (tl->parts[n].source == ctx->demuxer) {
|
||||
tl->track_layout = ctx->demuxer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
check_track_compatibility(mpctx);
|
||||
check_track_compatibility(tl);
|
||||
}
|
||||
@@ -23,6 +23,9 @@
|
||||
#ifndef MPLAYER_MATROSKA_H
|
||||
#define MPLAYER_MATROSKA_H
|
||||
|
||||
struct timeline;
|
||||
void build_ordered_chapter_timeline(struct timeline *tl);
|
||||
|
||||
#define MKV_A_AAC_2MAIN "A_AAC/MPEG2/MAIN"
|
||||
#define MKV_A_AAC_2LC "A_AAC/MPEG2/LC"
|
||||
#define MKV_A_AAC_2SBR "A_AAC/MPEG2/LC/SBR"
|
||||
|
||||
@@ -83,7 +83,6 @@ typedef struct sh_video {
|
||||
float aspect; // aspect ratio stored in the file (for prescaling)
|
||||
int bitrate; // compressed bits/sec
|
||||
int bits_per_coded_sample;
|
||||
int coded_width, coded_height;
|
||||
unsigned char *extradata;
|
||||
int extradata_len;
|
||||
int disp_w, disp_h; // display size
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "common/common.h"
|
||||
#include "stream/stream.h"
|
||||
#include "demux.h"
|
||||
|
||||
#include "timeline.h"
|
||||
|
||||
struct timeline *timeline_load(struct mpv_global *global, struct mp_log *log,
|
||||
struct demuxer *demuxer)
|
||||
{
|
||||
if (!demuxer->desc->load_timeline)
|
||||
return NULL;
|
||||
|
||||
struct timeline *tl = talloc_ptrtype(NULL, tl);
|
||||
*tl = (struct timeline){
|
||||
.global = global,
|
||||
.log = log,
|
||||
.demuxer = demuxer,
|
||||
.track_layout = demuxer,
|
||||
};
|
||||
|
||||
demuxer->desc->load_timeline(tl);
|
||||
|
||||
if (tl->num_parts)
|
||||
return tl;
|
||||
timeline_destroy(tl);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void timeline_destroy(struct timeline *tl)
|
||||
{
|
||||
if (!tl)
|
||||
return;
|
||||
for (int n = 0; n < tl->num_sources; n++) {
|
||||
struct demuxer *d = tl->sources[n];
|
||||
if (d != tl->demuxer) {
|
||||
struct stream *s = d->stream;
|
||||
free_demuxer(d);
|
||||
free_stream(s);
|
||||
}
|
||||
}
|
||||
talloc_free(tl);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef MP_TIMELINE_H_
|
||||
#define MP_TIMELINE_H_
|
||||
|
||||
struct timeline_part {
|
||||
double start;
|
||||
double source_start;
|
||||
struct demuxer *source;
|
||||
};
|
||||
|
||||
struct timeline {
|
||||
struct mpv_global *global;
|
||||
struct mp_log *log;
|
||||
|
||||
// main source
|
||||
struct demuxer *demuxer;
|
||||
|
||||
// All referenced files. The source file must be at sources[0].
|
||||
struct demuxer **sources;
|
||||
int num_sources;
|
||||
|
||||
// Segments to play, ordered by time. parts[num_parts] must be valid; its
|
||||
// start field sets the duration, and source must be NULL.
|
||||
struct timeline_part *parts;
|
||||
int num_parts;
|
||||
|
||||
struct demux_chapter *chapters;
|
||||
int num_chapters;
|
||||
|
||||
// Which source defines the overall track list (over the full timeline).
|
||||
struct demuxer *track_layout;
|
||||
};
|
||||
|
||||
struct timeline *timeline_load(struct mpv_global *global, struct mp_log *log,
|
||||
struct demuxer *demuxer);
|
||||
void timeline_destroy(struct timeline *tl);
|
||||
|
||||
#endif
|
||||
@@ -213,5 +213,3 @@
|
||||
# ? cycle sub-forced-only # toggle DVD forced subs
|
||||
# ? cycle program # cycle transport stream programs
|
||||
# ? stop # stop playback (quit or enter idle mode)
|
||||
# Cycle aspect ratio:
|
||||
# ? cycle_values video-aspect "16:9" "16:10" "4:3" "2.35:1" "-1"
|
||||
@@ -154,6 +154,13 @@ extern "C" {
|
||||
* "wid" option fully and letterbox the video (i.e. add black bars if the
|
||||
* aspect ratio of the window and the video mismatch).
|
||||
*
|
||||
* Setting the "input-vo-keyboard" may be required to get keyboard input
|
||||
* through the embedded window, if this is desired.
|
||||
*
|
||||
* For OpenGL integration (e.g. rendering video to a texture), a separate API
|
||||
* is available. Look at opengl_cb.h. This API does not include keyboard or
|
||||
* mouse input directly.
|
||||
*
|
||||
* Also see client API examples and the mpv manpage.
|
||||
*
|
||||
* Compatibility
|
||||
|
||||
+1
-1
@@ -509,7 +509,7 @@ check_statement_libs "compiler support for __sync built-ins" $_sync SYNC_BUILTIN
|
||||
_sync=$(defretval)
|
||||
|
||||
if test "$_atomic" = no && test "$_sync" = no && test "$_stdatomic" = no ; then
|
||||
die "your compiler must support either stdatomic.h, or __atomic, or __sync built-ins."
|
||||
echo "your compiler must support either stdatomic.h, or __atomic, or __sync built-ins."
|
||||
fi
|
||||
|
||||
check_compile "iconv" $_iconv ICONV waftools/fragments/iconv.c " " "-liconv" "-liconv $_ld_dl"
|
||||
|
||||
+2
-3
@@ -171,11 +171,13 @@ SOURCES = audio/audio.c \
|
||||
demux/demux_lavf.c \
|
||||
demux/demux_mf.c \
|
||||
demux/demux_mkv.c \
|
||||
demux/demux_mkv_timeline.c \
|
||||
demux/demux_playlist.c \
|
||||
demux/demux_raw.c \
|
||||
demux/demux_subreader.c \
|
||||
demux/ebml.c \
|
||||
demux/packet.c \
|
||||
demux/timeline.c \
|
||||
input/cmd_list.c \
|
||||
input/cmd_parse.c \
|
||||
input/event.c \
|
||||
@@ -216,9 +218,6 @@ SOURCES = audio/audio.c \
|
||||
player/scripting.c \
|
||||
player/sub.c \
|
||||
player/video.c \
|
||||
player/timeline/tl_matroska.c \
|
||||
player/timeline/tl_mpv_edl.c \
|
||||
player/timeline/tl_cue.c \
|
||||
stream/cache.c \
|
||||
stream/cache_file.c \
|
||||
stream/cookies.c \
|
||||
|
||||
@@ -717,6 +717,13 @@ int m_config_set_option_node(struct m_config *config, bstr name,
|
||||
if (r >= 0)
|
||||
r = m_config_set_option_raw(config, co, &val, flags);
|
||||
|
||||
if (mp_msg_test(config->log, MSGL_V)) {
|
||||
char *s = m_option_type_node.print(NULL, data);
|
||||
MP_VERBOSE(config, "Setting option '%.*s' = %s (flags = %d) -> %d\n",
|
||||
BSTR_P(name), s ? s : "?", flags, r);
|
||||
talloc_free(s);
|
||||
}
|
||||
|
||||
m_option_free(co->opt, &val);
|
||||
return r;
|
||||
}
|
||||
|
||||
+6
-17
@@ -26,6 +26,7 @@
|
||||
#include "common/common.h"
|
||||
#include "options/options.h"
|
||||
#include "sub/osd.h"
|
||||
#include "demux/timeline.h"
|
||||
|
||||
// definitions used internally by the core player code
|
||||
|
||||
@@ -41,12 +42,6 @@ enum stop_play_reason {
|
||||
PT_ERROR, // play next playlist entry (due to an error)
|
||||
};
|
||||
|
||||
struct timeline_part {
|
||||
double start;
|
||||
double source_start;
|
||||
struct demuxer *source;
|
||||
};
|
||||
|
||||
enum mp_osd_seek_info {
|
||||
OSD_SEEK_INFO_BAR = 1,
|
||||
OSD_SEEK_INFO_TEXT = 2,
|
||||
@@ -185,9 +180,10 @@ typedef struct MPContext {
|
||||
int64_t shown_vframes, shown_aframes;
|
||||
|
||||
struct stream *stream; // stream that was initially opened
|
||||
struct demuxer **sources;
|
||||
struct demuxer **sources; // all open demuxers
|
||||
int num_sources;
|
||||
|
||||
struct timeline *tl;
|
||||
struct timeline_part *timeline;
|
||||
int num_timeline_parts;
|
||||
int timeline_part;
|
||||
@@ -195,7 +191,7 @@ typedef struct MPContext {
|
||||
int num_chapters;
|
||||
double video_offset;
|
||||
|
||||
struct demuxer *demuxer;
|
||||
struct demuxer *demuxer; // can change with timeline
|
||||
struct mp_tags *filtered_tags;
|
||||
|
||||
struct track **tracks;
|
||||
@@ -416,8 +412,8 @@ bool mp_get_cache_idle(struct MPContext *mpctx);
|
||||
void update_window_title(struct MPContext *mpctx, bool force);
|
||||
void error_on_track(struct MPContext *mpctx, struct track *track);
|
||||
void stream_dump(struct MPContext *mpctx);
|
||||
int mpctx_run_non_blocking(struct MPContext *mpctx, void (*thread_fn)(void *arg),
|
||||
void *thread_arg);
|
||||
int mpctx_run_reentrant(struct MPContext *mpctx, void (*thread_fn)(void *arg),
|
||||
void *thread_arg);
|
||||
struct mpv_global *create_sub_global(struct MPContext *mpctx);
|
||||
|
||||
// osd.c
|
||||
@@ -478,13 +474,6 @@ void uninit_sub_renderer(struct MPContext *mpctx);
|
||||
void update_osd_sub_state(struct MPContext *mpctx, int order,
|
||||
struct osd_sub_state *out_state);
|
||||
|
||||
// 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
|
||||
void reset_video_state(struct MPContext *mpctx);
|
||||
int reinit_video_chain(struct MPContext *mpctx);
|
||||
|
||||
+93
-54
@@ -63,33 +63,41 @@ static void uninit_demuxer(struct MPContext *mpctx)
|
||||
{
|
||||
assert(!mpctx->d_video && !mpctx->d_audio &&
|
||||
!mpctx->d_sub[0] && !mpctx->d_sub[1]);
|
||||
for (int i = 0; i < mpctx->num_tracks; i++) {
|
||||
talloc_free(mpctx->tracks[i]);
|
||||
}
|
||||
mpctx->num_tracks = 0;
|
||||
for (int r = 0; r < NUM_PTRACKS; r++) {
|
||||
for (int t = 0; t < STREAM_TYPE_COUNT; t++)
|
||||
mpctx->current_track[r][t] = NULL;
|
||||
}
|
||||
mpctx->master_demuxer = NULL;
|
||||
for (int i = 0; i < mpctx->num_sources; i++) {
|
||||
uninit_stream_sub_decoders(mpctx->sources[i]);
|
||||
struct demuxer *demuxer = mpctx->sources[i];
|
||||
struct stream *stream = demuxer->stream;
|
||||
free_demuxer(demuxer);
|
||||
if (stream != mpctx->stream)
|
||||
free_stream(stream);
|
||||
}
|
||||
talloc_free(mpctx->sources);
|
||||
mpctx->sources = NULL;
|
||||
mpctx->track_layout = NULL;
|
||||
mpctx->demuxer = NULL;
|
||||
mpctx->num_sources = 0;
|
||||
talloc_free(mpctx->timeline);
|
||||
mpctx->timeline = NULL;
|
||||
mpctx->num_timeline_parts = 0;
|
||||
talloc_free(mpctx->chapters);
|
||||
mpctx->chapters = NULL;
|
||||
mpctx->num_chapters = 0;
|
||||
|
||||
// per-stream cached subtitle state
|
||||
for (int i = 0; i < mpctx->num_sources; i++)
|
||||
uninit_stream_sub_decoders(mpctx->sources[i]);
|
||||
|
||||
// close demuxers for external tracks
|
||||
for (int n = mpctx->num_tracks - 1; n >= 0; n--) {
|
||||
mpctx->tracks[n]->selected = false;
|
||||
mp_remove_track(mpctx, mpctx->tracks[n]);
|
||||
}
|
||||
for (int i = 0; i < mpctx->num_tracks; i++)
|
||||
talloc_free(mpctx->tracks[i]);
|
||||
mpctx->num_tracks = 0;
|
||||
|
||||
mpctx->timeline = NULL;
|
||||
mpctx->num_timeline_parts = 0;
|
||||
timeline_destroy(mpctx->tl);
|
||||
mpctx->tl = NULL;
|
||||
|
||||
free_demuxer(mpctx->master_demuxer);
|
||||
mpctx->master_demuxer = NULL;
|
||||
|
||||
talloc_free(mpctx->sources);
|
||||
mpctx->sources = NULL;
|
||||
mpctx->num_sources = 0;
|
||||
|
||||
mpctx->video_offset = 0;
|
||||
}
|
||||
|
||||
@@ -619,26 +627,35 @@ struct track *mp_track_by_tid(struct MPContext *mpctx, enum stream_type type,
|
||||
|
||||
bool mp_remove_track(struct MPContext *mpctx, struct track *track)
|
||||
{
|
||||
if (track->under_timeline)
|
||||
return false;
|
||||
if (!track->is_external)
|
||||
return false;
|
||||
assert(!track->under_timeline);
|
||||
|
||||
mp_deselect_track(mpctx, track);
|
||||
if (track->selected)
|
||||
return false;
|
||||
|
||||
struct demuxer *d = track->demuxer;
|
||||
|
||||
int index = 0;
|
||||
while (index < mpctx->num_tracks && mpctx->tracks[index] != track)
|
||||
index++;
|
||||
assert(index < mpctx->num_tracks);
|
||||
while (index + 1 < mpctx->num_tracks) {
|
||||
mpctx->tracks[index] = mpctx->tracks[index + 1];
|
||||
index++;
|
||||
}
|
||||
mpctx->num_tracks--;
|
||||
MP_TARRAY_REMOVE_AT(mpctx->tracks, mpctx->num_tracks, index);
|
||||
talloc_free(track);
|
||||
|
||||
// Close the demuxer, unless there is still a track using it. These are
|
||||
// all external tracks, so there are no complications due to the timeline
|
||||
// mechanism switching the track's demuxer dynamically.
|
||||
bool in_use = false;
|
||||
for (int n = mpctx->num_tracks - 1; n >= 0 && !in_use; n--)
|
||||
in_use |= mpctx->tracks[n]->demuxer == d;
|
||||
|
||||
if (!in_use) {
|
||||
struct stream *s = d->stream;
|
||||
free_demuxer(d);
|
||||
free_stream(s);
|
||||
}
|
||||
|
||||
mp_notify(mpctx, MPV_EVENT_TRACKS_CHANGED, NULL);
|
||||
|
||||
return true;
|
||||
@@ -906,8 +923,8 @@ static void open_stream_thread(void *pctx)
|
||||
args->cancel, args->global);
|
||||
}
|
||||
|
||||
static struct stream *open_stream_async(struct MPContext *mpctx,
|
||||
char *filename, int stream_flags)
|
||||
static struct stream *open_stream_reentrant(struct MPContext *mpctx,
|
||||
char *filename, int stream_flags)
|
||||
{
|
||||
struct stream_open_args args = {
|
||||
.cancel = mpctx->playback_abort,
|
||||
@@ -915,7 +932,7 @@ static struct stream *open_stream_async(struct MPContext *mpctx,
|
||||
.filename = filename,
|
||||
.stream_flags = stream_flags,
|
||||
};
|
||||
mpctx_run_non_blocking(mpctx, open_stream_thread, &args);
|
||||
mpctx_run_reentrant(mpctx, open_stream_thread, &args);
|
||||
if (args.stream) {
|
||||
talloc_steal(args.stream, args.global);
|
||||
} else {
|
||||
@@ -927,7 +944,10 @@ static struct stream *open_stream_async(struct MPContext *mpctx,
|
||||
struct demux_open_args {
|
||||
struct stream *stream;
|
||||
struct mpv_global *global;
|
||||
struct demuxer *demux; // result
|
||||
struct mp_log *log;
|
||||
// results
|
||||
struct demuxer *demux;
|
||||
struct timeline *tl;
|
||||
};
|
||||
|
||||
static void open_demux_thread(void *pctx)
|
||||
@@ -936,19 +956,50 @@ static void open_demux_thread(void *pctx)
|
||||
struct stream *s = args->stream;
|
||||
struct mpv_global *global = args->global;
|
||||
args->demux = demux_open(s, global->opts->demuxer_name, NULL, global);
|
||||
if (args->demux)
|
||||
args->tl = timeline_load(global, args->log, args->demux);
|
||||
}
|
||||
|
||||
static struct demuxer *open_demux_async(struct MPContext *mpctx,
|
||||
struct stream *stream)
|
||||
static void open_demux_reentrant(struct MPContext *mpctx)
|
||||
{
|
||||
struct demux_open_args args = {stream, create_sub_global(mpctx)};
|
||||
mpctx_run_non_blocking(mpctx, open_demux_thread, &args);
|
||||
struct demux_open_args args = {
|
||||
.global = create_sub_global(mpctx),
|
||||
.stream = mpctx->stream,
|
||||
.log = mpctx->log,
|
||||
};
|
||||
mpctx_run_reentrant(mpctx, open_demux_thread, &args);
|
||||
if (args.demux) {
|
||||
talloc_steal(args.demux, args.global);
|
||||
mpctx->master_demuxer = args.demux;
|
||||
mpctx->tl = args.tl;
|
||||
} else {
|
||||
talloc_free(args.global);
|
||||
}
|
||||
return args.demux;
|
||||
}
|
||||
|
||||
static void load_timeline(struct MPContext *mpctx)
|
||||
{
|
||||
mpctx->track_layout = mpctx->master_demuxer;
|
||||
|
||||
MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources,
|
||||
mpctx->master_demuxer);
|
||||
|
||||
if (mpctx->tl) {
|
||||
mpctx->timeline = mpctx->tl->parts;
|
||||
mpctx->num_timeline_parts = mpctx->tl->num_parts;
|
||||
mpctx->num_chapters = mpctx->tl->num_chapters;
|
||||
mpctx->chapters = demux_copy_chapter_data(mpctx->tl->chapters,
|
||||
mpctx->tl->num_chapters);
|
||||
mpctx->track_layout = mpctx->tl->track_layout;
|
||||
for (int n = 0; n < mpctx->tl->num_sources; n++) {
|
||||
if (mpctx->tl->sources[n] != mpctx->master_demuxer) {
|
||||
MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources,
|
||||
mpctx->tl->sources[n]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print_timeline(mpctx);
|
||||
}
|
||||
|
||||
// Start playing the current playlist entry.
|
||||
@@ -1029,8 +1080,8 @@ static void play_current_file(struct MPContext *mpctx)
|
||||
int stream_flags = STREAM_READ;
|
||||
if (!opts->load_unsafe_playlists)
|
||||
stream_flags |= mpctx->playing->stream_flags;
|
||||
mpctx->stream = open_stream_async(mpctx, mpctx->stream_open_filename,
|
||||
stream_flags);
|
||||
mpctx->stream = open_stream_reentrant(mpctx, mpctx->stream_open_filename,
|
||||
stream_flags);
|
||||
if (!mpctx->stream)
|
||||
goto terminate_playback;
|
||||
|
||||
@@ -1056,15 +1107,15 @@ goto_reopen_demuxer: ;
|
||||
|
||||
mp_nav_reset(mpctx);
|
||||
|
||||
mpctx->demuxer = open_demux_async(mpctx, mpctx->stream);
|
||||
if (!mpctx->demuxer) {
|
||||
open_demux_reentrant(mpctx);
|
||||
if (!mpctx->master_demuxer) {
|
||||
MP_ERR(mpctx, "Failed to recognize file format.\n");
|
||||
mpctx->error_playing = MPV_ERROR_UNKNOWN_FORMAT;
|
||||
goto terminate_playback;
|
||||
}
|
||||
mpctx->master_demuxer = mpctx->demuxer;
|
||||
mpctx->demuxer = mpctx->master_demuxer;
|
||||
|
||||
MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources, mpctx->demuxer);
|
||||
load_timeline(mpctx);
|
||||
|
||||
if (mpctx->demuxer->playlist) {
|
||||
int entry_stream_flags =
|
||||
@@ -1079,18 +1130,6 @@ goto_reopen_demuxer: ;
|
||||
goto terminate_playback;
|
||||
}
|
||||
|
||||
mpctx->track_layout = mpctx->demuxer;
|
||||
|
||||
if (mpctx->demuxer->matroska_data.ordered_chapters)
|
||||
build_ordered_chapter_timeline(mpctx);
|
||||
|
||||
if (mpctx->demuxer->type == DEMUXER_TYPE_EDL)
|
||||
build_mpv_edl_timeline(mpctx);
|
||||
|
||||
if (mpctx->demuxer->type == DEMUXER_TYPE_CUE)
|
||||
build_cue_timeline(mpctx);
|
||||
|
||||
print_timeline(mpctx);
|
||||
load_chapters(mpctx);
|
||||
add_demuxer_tracks(mpctx, mpctx->track_layout);
|
||||
|
||||
|
||||
+12
-10
@@ -3,7 +3,7 @@ local msg = require 'mp.msg'
|
||||
|
||||
local ytdl = {
|
||||
path = "youtube-dl",
|
||||
minver = "2014.11.26",
|
||||
minver = "2015.02.23.1",
|
||||
vercheck = nil,
|
||||
}
|
||||
|
||||
@@ -82,13 +82,10 @@ mp.add_hook("on_load", 10, function ()
|
||||
local format = mp.get_property("options/ytdl-format")
|
||||
|
||||
-- subformat workaround
|
||||
local subformat = "srt"
|
||||
if url:find("crunchyroll.com") then
|
||||
subformat = "ass"
|
||||
end
|
||||
local subformat = "ass/srt/best"
|
||||
|
||||
local command = {
|
||||
ytdl.path, "-J", "--flat-playlist", "--all-subs",
|
||||
ytdl.path, "--no-warnings", "-J", "--flat-playlist", "--all-subs",
|
||||
"--sub-format", subformat, "--no-playlist"
|
||||
}
|
||||
if (format ~= "") then
|
||||
@@ -209,8 +206,8 @@ mp.add_hook("on_load", 10, function ()
|
||||
mp.set_property("file-local-options/media-title", json.title)
|
||||
|
||||
-- add subtitles
|
||||
if not (json.subtitles == nil) then
|
||||
for lang, script in pairs(json.subtitles) do
|
||||
if not (json.requested_subtitles == nil) then
|
||||
for lang, sub_info in pairs(json.requested_subtitles) do
|
||||
msg.verbose("adding subtitle ["..lang.."]")
|
||||
|
||||
local slang = lang
|
||||
@@ -218,8 +215,13 @@ mp.add_hook("on_load", 10, function ()
|
||||
slang = lang:sub(1,2)
|
||||
end
|
||||
|
||||
mp.commandv("sub_add", "memory://"..script,
|
||||
"auto", lang.." "..subformat, slang)
|
||||
if not (sub_info.data == nil) then
|
||||
sub = "memory://"..sub_info.data
|
||||
else
|
||||
sub = sub_info.url
|
||||
end
|
||||
mp.commandv("sub_add", sub,
|
||||
"auto", lang.." "..sub_info.ext, slang)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+3
-1
@@ -408,7 +408,9 @@ int mp_initialize(struct MPContext *mpctx)
|
||||
m_config_set_option0(mpctx->mconfig, "load-scripts", "no");
|
||||
m_config_set_option0(mpctx->mconfig, "osc", "no");
|
||||
m_config_set_option0(mpctx->mconfig, "framedrop", "no");
|
||||
m_config_set_option0(mpctx->mconfig, "audio-channels", "stereo");
|
||||
// never use auto
|
||||
if (!opts->audio_output_channels.num)
|
||||
m_config_set_option0(mpctx->mconfig, "audio-channels", "stereo");
|
||||
mp_input_enable_section(mpctx->input, "encode", MP_INPUT_EXCLUSIVE);
|
||||
}
|
||||
#endif
|
||||
|
||||
+2
-2
@@ -283,8 +283,8 @@ static void *thread_wrapper(void *pctx)
|
||||
|
||||
// Run the thread_fn in a new thread. Wait until the thread returns, but while
|
||||
// waiting, process input and input commands.
|
||||
int mpctx_run_non_blocking(struct MPContext *mpctx, void (*thread_fn)(void *arg),
|
||||
void *thread_arg)
|
||||
int mpctx_run_reentrant(struct MPContext *mpctx, void (*thread_fn)(void *arg),
|
||||
void *thread_arg)
|
||||
{
|
||||
struct wrapper_args args = {mpctx, thread_fn, thread_arg};
|
||||
pthread_mutex_init(&args.mutex, NULL);
|
||||
|
||||
@@ -1,415 +0,0 @@
|
||||
/*
|
||||
* 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 "talloc.h"
|
||||
|
||||
#include "player/core.h"
|
||||
#include "common/msg.h"
|
||||
#include "demux/demux.h"
|
||||
#include "options/path.h"
|
||||
#include "misc/bstr.h"
|
||||
#include "common/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->global);
|
||||
if (!s)
|
||||
return false;
|
||||
struct demuxer *d = demux_open(s, NULL, NULL, mpctx->global);
|
||||
// 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_WARN(mpctx, "CUE: Opening as BIN file!\n");
|
||||
d = demux_open(s, "rawaudio", NULL, mpctx->global);
|
||||
}
|
||||
if (d) {
|
||||
add_source(mpctx, d);
|
||||
return true;
|
||||
}
|
||||
MP_ERR(mpctx, "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_WARN(mpctx, "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_WARN(mpctx, "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_ERR(mpctx, "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_ERR(mpctx, "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_ERR(mpctx, "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 demux_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_WARN(mpctx, "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 demux_chapter) {
|
||||
.pts = 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;
|
||||
mpctx->track_layout = mpctx->timeline[0].source;
|
||||
|
||||
out:
|
||||
talloc_free(ctx);
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
/*
|
||||
* 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 <math.h>
|
||||
|
||||
#include "talloc.h"
|
||||
|
||||
#include "player/core.h"
|
||||
#include "common/msg.h"
|
||||
#include "demux/demux.h"
|
||||
#include "options/path.h"
|
||||
#include "misc/bstr.h"
|
||||
#include "common/common.h"
|
||||
#include "stream/stream.h"
|
||||
|
||||
struct tl_part {
|
||||
char *filename; // what is stream_open()ed
|
||||
double offset; // offset into the source file
|
||||
bool offset_set;
|
||||
bool chapter_ts;
|
||||
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;
|
||||
p.offset_set = true;
|
||||
} else if (bstr_equals0(name, "length")) {
|
||||
if (!parse_time(val, &p.length))
|
||||
goto error;
|
||||
} else if (bstr_equals0(name, "timestamps")) {
|
||||
if (bstr_equals0(val, "chapters"))
|
||||
p.chapter_ts = true;
|
||||
}
|
||||
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, mpctx->global);
|
||||
if (s) {
|
||||
stream_enable_cache(&s, &opts->stream_cache);
|
||||
d = demux_open(s, NULL, NULL, mpctx->global);
|
||||
}
|
||||
if (!d) {
|
||||
MP_ERR(mpctx, "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;
|
||||
}
|
||||
|
||||
static double demuxer_chapter_time(struct demuxer *demuxer, int n)
|
||||
{
|
||||
if (n < 0 || n >= demuxer->num_chapters)
|
||||
return -1;
|
||||
return demuxer->chapters[n].pts;
|
||||
}
|
||||
|
||||
// Append all chapters from src to the chapters array.
|
||||
// Ignore chapters outside of the given time range.
|
||||
static void copy_chapters(struct demux_chapter **chapters, int *num_chapters,
|
||||
struct demuxer *src, double start, double len,
|
||||
double dest_offset)
|
||||
{
|
||||
for (int n = 0; n < src->num_chapters; n++) {
|
||||
double time = demuxer_chapter_time(src, n);
|
||||
if (time >= start && time <= start + len) {
|
||||
struct demux_chapter ch = {
|
||||
.pts = dest_offset + time - start,
|
||||
.name = talloc_strdup(*chapters, src->chapters[n].name),
|
||||
};
|
||||
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 resolve_timestamps(struct tl_part *part, struct demuxer *demuxer)
|
||||
{
|
||||
if (part->chapter_ts) {
|
||||
double start = demuxer_chapter_time(demuxer, part->offset);
|
||||
double length = part->length;
|
||||
double end = length;
|
||||
if (end >= 0)
|
||||
end = demuxer_chapter_time(demuxer, part->offset + part->length);
|
||||
if (end >= 0 && start >= 0)
|
||||
length = end - start;
|
||||
part->offset = start;
|
||||
part->length = length;
|
||||
}
|
||||
if (!part->offset_set)
|
||||
part->offset = demuxer->start_time;
|
||||
}
|
||||
|
||||
static void build_timeline(struct MPContext *mpctx, struct tl_parts *parts)
|
||||
{
|
||||
struct demux_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;
|
||||
|
||||
resolve_timestamps(part, source);
|
||||
|
||||
double len = source_get_length(source);
|
||||
if (len > 0) {
|
||||
len += source->start_time;
|
||||
} else {
|
||||
MP_WARN(mpctx, "EDL: source file '%s' has unknown duration.\n",
|
||||
part->filename);
|
||||
}
|
||||
|
||||
// Unknown 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_WARN(mpctx, "EDL: entry %d uses %f "
|
||||
"seconds, but file has only %f seconds.\n",
|
||||
n, partlen, len);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a chapter between each file.
|
||||
struct demux_chapter ch = {
|
||||
.pts = 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;
|
||||
mpctx->track_layout = mpctx->timeline[0].source;
|
||||
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_ERR(mpctx, "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);
|
||||
}
|
||||
+11
-11
@@ -126,13 +126,6 @@ enum {
|
||||
FILL_LIMIT = 16 * 1024,
|
||||
};
|
||||
|
||||
static int64_t mp_clipi64(int64_t val, int64_t min, int64_t max)
|
||||
{
|
||||
val = FFMIN(val, max);
|
||||
val = FFMAX(val, min);
|
||||
return val;
|
||||
}
|
||||
|
||||
// Used by the main thread to wakeup the cache thread, and to wait for the
|
||||
// cache thread. The cache mutex has to be locked when calling this function.
|
||||
// *retry_time should be set to 0 on the first call.
|
||||
@@ -232,7 +225,7 @@ static bool cache_fill(struct priv *s)
|
||||
}
|
||||
|
||||
// number of buffer bytes which should be preserved in backwards direction
|
||||
int64_t back = mp_clipi64(read - s->min_filepos, 0, s->back_size);
|
||||
int64_t back = MPCLAMP(read - s->min_filepos, 0, s->back_size);
|
||||
|
||||
// number of buffer bytes that are valid and can be read
|
||||
int64_t newb = FFMAX(s->max_filepos - read, 0);
|
||||
@@ -285,7 +278,7 @@ done:
|
||||
s->idle = s->eof;
|
||||
s->reads++;
|
||||
if (s->eof)
|
||||
MP_VERBOSE(s, "EOF reached.\n");
|
||||
MP_TRACE(s, "EOF reached.\n");
|
||||
|
||||
pthread_cond_signal(&s->wakeup);
|
||||
|
||||
@@ -297,7 +290,7 @@ static int resize_cache(struct priv *s, int64_t size)
|
||||
{
|
||||
int64_t min_size = FILL_LIMIT * 4;
|
||||
int64_t max_size = ((size_t)-1) / 4;
|
||||
int64_t buffer_size = MPMIN(MPMAX(size, min_size), max_size);
|
||||
int64_t buffer_size = MPCLAMP(size, min_size, max_size);
|
||||
|
||||
unsigned char *buffer = malloc(buffer_size);
|
||||
if (!buffer) {
|
||||
@@ -622,7 +615,14 @@ int stream_cache_init(stream_t *cache, stream_t *stream,
|
||||
|
||||
s->seek_limit = opts->seek_min * 1024ULL;
|
||||
|
||||
if (resize_cache(s, opts->size * 1024ULL) != STREAM_OK) {
|
||||
int64_t cache_size = opts->size * 1024ULL;
|
||||
|
||||
int64_t file_size = -1;
|
||||
stream_control(stream, STREAM_CTRL_GET_SIZE, &file_size);
|
||||
if (file_size >= 0)
|
||||
cache_size = MPMIN(cache_size, file_size);
|
||||
|
||||
if (resize_cache(s, cache_size) != STREAM_OK) {
|
||||
MP_ERR(s, "Failed to allocate cache buffer.\n");
|
||||
talloc_free(s);
|
||||
return -1;
|
||||
|
||||
+33
-4
@@ -27,6 +27,10 @@
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
#ifndef __MINGW32__
|
||||
#include <poll.h>
|
||||
#endif
|
||||
|
||||
#include "osdep/io.h"
|
||||
|
||||
#include "common/common.h"
|
||||
@@ -57,11 +61,24 @@
|
||||
struct priv {
|
||||
int fd;
|
||||
bool close;
|
||||
bool regular;
|
||||
};
|
||||
|
||||
static int fill_buffer(stream_t *s, char *buffer, int max_len)
|
||||
{
|
||||
struct priv *p = s->priv;
|
||||
#ifndef __MINGW32__
|
||||
if (!p->regular) {
|
||||
int c = s->cancel ? mp_cancel_get_fd(s->cancel) : -1;
|
||||
struct pollfd fds[2] = {
|
||||
{.fd = p->fd, .events = POLLIN},
|
||||
{.fd = c, .events = POLLIN},
|
||||
};
|
||||
poll(fds, c >= 0 ? 2 : 1, -1);
|
||||
if (fds[1].revents & POLLIN)
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
int r = read(p->fd, buffer, max_len);
|
||||
return (r <= 0) ? -1 : r;
|
||||
}
|
||||
@@ -250,6 +267,8 @@ static int open_f(stream_t *stream)
|
||||
mode_t openmode = S_IRUSR | S_IWUSR;
|
||||
#ifndef __MINGW32__
|
||||
openmode |= S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
|
||||
if (!write)
|
||||
m |= O_NONBLOCK;
|
||||
#endif
|
||||
fd = open(filename, m | O_BINARY, openmode);
|
||||
if (fd < 0) {
|
||||
@@ -258,10 +277,20 @@ static int open_f(stream_t *stream)
|
||||
return STREAM_ERROR;
|
||||
}
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
MP_ERR(stream, "File is a directory: '%s'\n", filename);
|
||||
close(fd);
|
||||
return STREAM_ERROR;
|
||||
if (fstat(fd, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
MP_ERR(stream, "File is a directory: '%s'\n", filename);
|
||||
close(fd);
|
||||
return STREAM_ERROR;
|
||||
}
|
||||
#ifndef __MINGW32__
|
||||
if (S_ISREG(st.st_mode)) {
|
||||
priv->regular = true;
|
||||
// O_NONBLOCK has weird semantics on file locks; remove it.
|
||||
int val = fcntl(fd, F_GETFL) & ~(unsigned)O_NONBLOCK;
|
||||
fcntl(fd, F_SETFL, val);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
priv->fd = fd;
|
||||
priv->close = true;
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
static const char *const sub_exts[] = {"utf", "utf8", "utf-8", "idx", "sub", "srt",
|
||||
"smi", "rt", "txt", "ssa", "aqt", "jss",
|
||||
"js", "ass", "mks", NULL};
|
||||
"js", "ass", "mks", "vtt", NULL};
|
||||
|
||||
static const char *const audio_exts[] = {"mp3", "aac", "mka", "dts", "flac",
|
||||
"ogg", "m4a", NULL};
|
||||
|
||||
+8
-11
@@ -301,7 +301,6 @@ static void get_osd_bar_box(struct osd_state *osd, struct osd_object *obj,
|
||||
{
|
||||
struct MPOpts *opts = osd->opts;
|
||||
|
||||
bool new_track = !obj->osd_track;
|
||||
create_ass_track(osd, obj, 0, 0);
|
||||
ASS_Track *track = obj->osd_track;
|
||||
ASS_Style *style = track->styles + track->default_style;
|
||||
@@ -309,16 +308,14 @@ static void get_osd_bar_box(struct osd_state *osd, struct osd_object *obj,
|
||||
*o_w = track->PlayResX * (opts->osd_bar_w / 100.0);
|
||||
*o_h = track->PlayResY * (opts->osd_bar_h / 100.0);
|
||||
|
||||
if (new_track) {
|
||||
float base_size = 0.03125;
|
||||
style->Outline *= *o_h / track->PlayResY / base_size;
|
||||
// So that the chapter marks have space between them
|
||||
style->Outline = FFMIN(style->Outline, *o_h / 5.2);
|
||||
// So that the border is not 0
|
||||
style->Outline = FFMAX(style->Outline, *o_h / 32.0);
|
||||
// Rendering with shadow is broken (because there's more than one shape)
|
||||
style->Shadow = 0;
|
||||
}
|
||||
float base_size = 0.03125;
|
||||
style->Outline *= *o_h / track->PlayResY / base_size;
|
||||
// So that the chapter marks have space between them
|
||||
style->Outline = FFMIN(style->Outline, *o_h / 5.2);
|
||||
// So that the border is not 0
|
||||
style->Outline = FFMAX(style->Outline, *o_h / 32.0);
|
||||
// Rendering with shadow is broken (because there's more than one shape)
|
||||
style->Shadow = 0;
|
||||
|
||||
*o_border = style->Outline;
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ struct priv {
|
||||
int rt_format;
|
||||
|
||||
struct mp_image_pool *sw_pool;
|
||||
bool printed_readback_warning;
|
||||
};
|
||||
|
||||
#define PE(av_codec_id, ff_profile, vdp_profile) \
|
||||
@@ -450,10 +449,6 @@ static struct mp_image *copy_image(struct lavc_ctx *ctx, struct mp_image *img)
|
||||
|
||||
struct mp_image *simg = va_surface_download(img, p->sw_pool);
|
||||
if (simg) {
|
||||
if (!p->printed_readback_warning) {
|
||||
MP_WARN(p, "Using GPU readback. This is usually inefficient.\n");
|
||||
p->printed_readback_warning = true;
|
||||
}
|
||||
talloc_free(img);
|
||||
return simg;
|
||||
}
|
||||
|
||||
@@ -402,11 +402,6 @@ static void init_avctx(struct dec_video *vd, const char *decoder,
|
||||
avctx->coded_height = sh->video->disp_h;
|
||||
avctx->bits_per_coded_sample = sh->video->bits_per_coded_sample;
|
||||
|
||||
if (sh->video->coded_width && sh->video->coded_height) {
|
||||
avctx->coded_width = sh->video->coded_width;
|
||||
avctx->coded_height = sh->video->coded_height;
|
||||
}
|
||||
|
||||
mp_lavc_set_extradata(avctx, sh->video->extradata, sh->video->extradata_len);
|
||||
|
||||
if (mp_rawvideo) {
|
||||
|
||||
+2
-2
@@ -226,11 +226,11 @@ void vf_print_filter_chain(struct vf_chain *c, int msglevel,
|
||||
char b[128] = {0};
|
||||
|
||||
mp_snprintf_cat(b, sizeof(b), "%s", mp_image_params_to_str(&c->input_params));
|
||||
mp_msg(c->log, msglevel, " [vd] %s\n", b);
|
||||
mp_msg(c->log, msglevel, " [vd] %s\n", b);
|
||||
|
||||
for (vf_instance_t *f = c->first; f; f = f->next) {
|
||||
b[0] = '\0';
|
||||
mp_snprintf_cat(b, sizeof(b), " [%s] ", f->info->name);
|
||||
mp_snprintf_cat(b, sizeof(b), " [%s] ", f->info->name);
|
||||
mp_snprintf_cat(b, sizeof(b), "%s", mp_image_params_to_str(&f->fmt_out));
|
||||
if (f->autoinserted)
|
||||
mp_snprintf_cat(b, sizeof(b), " [a]");
|
||||
|
||||
@@ -297,43 +297,14 @@ static double lanczos(kernel *k, double x)
|
||||
static double ewa_lanczos(kernel *k, double x)
|
||||
{
|
||||
double radius = k->radius;
|
||||
assert(radius >= 1.0);
|
||||
|
||||
// This is already three orders of magnitude slower than anything you could
|
||||
// possibly hope to play back in realtime and results in tons of ringing
|
||||
// artifacts, so I doubt anybody will complain.
|
||||
if (radius > 16)
|
||||
radius = 16;
|
||||
|
||||
if (fabs(x) < 1e-8)
|
||||
return 1.0;
|
||||
if (fabs(x) >= radius)
|
||||
return 0.0;
|
||||
|
||||
// Precomputed zeros of the jinc() function, needed to adjust the
|
||||
// window size. Computing this at runtime is nontrivial.
|
||||
// Copied from: https://github.com/AviSynth/jinc-resize/blob/master/JincResize/JincFilter.cpp#L171
|
||||
static double jinc_zeros[16] = {
|
||||
1.2196698912665045,
|
||||
2.2331305943815286,
|
||||
3.2383154841662362,
|
||||
4.2410628637960699,
|
||||
5.2427643768701817,
|
||||
6.2439216898644877,
|
||||
7.2447598687199570,
|
||||
8.2453949139520427,
|
||||
9.2458926849494673,
|
||||
10.246293348754916,
|
||||
11.246622794877883,
|
||||
12.246898461138105,
|
||||
13.247132522181061,
|
||||
14.247333735806849,
|
||||
15.247508563037300,
|
||||
16.247661874700962
|
||||
};
|
||||
|
||||
double window = jinc_zeros[0] / jinc_zeros[(int)radius - 1];
|
||||
return jinc(k, x) * jinc(k, x*window);
|
||||
// First zero of the jinc function. We simply scale it to fit into the
|
||||
// given radius.
|
||||
double jinc_zero = 1.2196698912665045;
|
||||
return jinc(k, x) * jinc(k, x * jinc_zero / radius);
|
||||
}
|
||||
|
||||
static double blackman(kernel *k, double x)
|
||||
|
||||
@@ -342,11 +342,15 @@ void mpgl_load_functions2(GL *gl, void *(*get_fn)(void *ctx, const char *n),
|
||||
|
||||
int major = 0, minor = 0;
|
||||
const char *version_string = gl->GetString(GL_VERSION);
|
||||
if (!version_string)
|
||||
goto error;
|
||||
mp_verbose(log, "GL_VERSION='%s'\n", version_string);
|
||||
if (strncmp(version_string, "OpenGL ES ", 10) == 0) {
|
||||
version_string += 10;
|
||||
gl->es = 100;
|
||||
}
|
||||
sscanf(version_string, "%d.%d", &major, &minor);
|
||||
if (sscanf(version_string, "%d.%d", &major, &minor) < 2)
|
||||
goto error;
|
||||
gl->version = MPGL_VER(major, minor);
|
||||
mp_verbose(log, "Detected %s %d.%d.\n", gl->es ? "GLES" : "desktop OpenGL",
|
||||
major, minor);
|
||||
@@ -362,7 +366,6 @@ void mpgl_load_functions2(GL *gl, void *(*get_fn)(void *ctx, const char *n),
|
||||
|
||||
mp_verbose(log, "GL_VENDOR='%s'\n", gl->GetString(GL_VENDOR));
|
||||
mp_verbose(log, "GL_RENDERER='%s'\n", gl->GetString(GL_RENDERER));
|
||||
mp_verbose(log, "GL_VERSION='%s'\n", gl->GetString(GL_VERSION));
|
||||
const char *shader = gl->GetString(GL_SHADING_LANGUAGE_VERSION);
|
||||
if (shader)
|
||||
mp_verbose(log, "GL_SHADING_LANGUAGE_VERSION='%s'\n", shader);
|
||||
|
||||
+153
-104
@@ -116,6 +116,7 @@ struct scaler {
|
||||
struct fbosurface {
|
||||
struct fbotex fbotex;
|
||||
int64_t pts;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
#define FBOSURFACES_MAX 2
|
||||
@@ -169,7 +170,6 @@ struct gl_video {
|
||||
|
||||
struct fbotex indirect_fbo; // RGB target
|
||||
struct fbotex scale_sep_fbo; // first pass when doing 2 pass scaling
|
||||
struct fbotex inter_fbo; // interpolation target
|
||||
struct fbosurface surfaces[FBOSURFACES_MAX];
|
||||
size_t surface_idx;
|
||||
|
||||
@@ -506,6 +506,13 @@ static void draw_quad(struct gl_video *p,
|
||||
debug_check_gl(p, "after rendering");
|
||||
}
|
||||
|
||||
static void transpose3x3(float r[3][3])
|
||||
{
|
||||
MPSWAP(float, r[0][1], r[1][0]);
|
||||
MPSWAP(float, r[0][2], r[2][0]);
|
||||
MPSWAP(float, r[1][2], r[2][1]);
|
||||
}
|
||||
|
||||
static void update_uniforms(struct gl_video *p, GLuint program)
|
||||
{
|
||||
GL *gl = p->gl;
|
||||
@@ -551,7 +558,8 @@ static void update_uniforms(struct gl_video *p, GLuint program)
|
||||
} else {
|
||||
mp_get_yuv2rgb_coeffs(&cparams, &m);
|
||||
}
|
||||
gl->UniformMatrix3fv(loc, 1, GL_TRUE, &m.m[0][0]);
|
||||
transpose3x3(m.m); // GLES2 can not transpose in glUniformMatrix3fv
|
||||
gl->UniformMatrix3fv(loc, 1, GL_FALSE, &m.m[0][0]);
|
||||
loc = gl->GetUniformLocation(program, "colormatrix_c");
|
||||
gl->Uniform3f(loc, m.c[0], m.c[1], m.c[2]);
|
||||
}
|
||||
@@ -1060,16 +1068,16 @@ static void compile_shaders(struct gl_video *p)
|
||||
header_sep = talloc_strdup(tmp, "");
|
||||
shader_def_opt(&header_sep, "FIXED_SCALE", true);
|
||||
shader_setup_scaler(&header_sep, &p->scalers[0], 0);
|
||||
shader_setup_scaler(&header_final, &p->scalers[0], 1);
|
||||
shader_setup_scaler(&header_inter, &p->scalers[0], 1);
|
||||
} else {
|
||||
shader_setup_scaler(&header_final, &p->scalers[0], -1);
|
||||
shader_setup_scaler(&header_inter, &p->scalers[0], -1);
|
||||
}
|
||||
|
||||
bool use_interpolation = p->opts.smoothmotion;
|
||||
|
||||
if (use_interpolation) {
|
||||
shader_def_opt(&header_inter, "FIXED_SCALE", true);
|
||||
shader_def_opt(&header_inter, "USE_LINEAR_INTERPOLATION", 1);
|
||||
shader_def_opt(&header_final, "USE_LINEAR_INTERPOLATION", 1);
|
||||
}
|
||||
|
||||
// The indirect pass is used to preprocess the image before scaling.
|
||||
@@ -1077,8 +1085,7 @@ static void compile_shaders(struct gl_video *p)
|
||||
|
||||
// Don't sample from input video textures before converting the input to
|
||||
// its proper gamma.
|
||||
if (use_input_gamma || use_conv_gamma || use_linear_light ||
|
||||
use_const_luma || use_interpolation)
|
||||
if (use_input_gamma || use_conv_gamma || use_linear_light || use_const_luma)
|
||||
use_indirect = true;
|
||||
|
||||
// Trivial scalers are implemented directly and efficiently by the GPU.
|
||||
@@ -1114,7 +1121,7 @@ static void compile_shaders(struct gl_video *p)
|
||||
} else if (header_sep) {
|
||||
header_sep = t_concat(tmp, header_sep, header_conv);
|
||||
} else {
|
||||
header_final = t_concat(tmp, header_final, header_conv);
|
||||
header_inter = t_concat(tmp, header_inter, header_conv);
|
||||
}
|
||||
|
||||
if (header_sep) {
|
||||
@@ -1127,6 +1134,8 @@ static void compile_shaders(struct gl_video *p)
|
||||
header_inter = t_concat(tmp, header, header_inter);
|
||||
p->inter_program =
|
||||
create_program(p, "inter", header_inter, vertex_shader, s_video, v);
|
||||
} else {
|
||||
header_final = t_concat(tmp, header_final, header_inter);
|
||||
}
|
||||
|
||||
header_final = t_concat(tmp, header, header_final);
|
||||
@@ -1418,18 +1427,6 @@ static void reinit_rendering(struct gl_video *p)
|
||||
p->opts.fbo_format);
|
||||
}
|
||||
|
||||
if (p->inter_program) {
|
||||
fbotex_init(&p->inter_fbo, gl, p->log, w, h, p->gl_target, filter,
|
||||
p->opts.fbo_format);
|
||||
}
|
||||
|
||||
if (p->inter_program) {
|
||||
for (int i = 0; i < FBOSURFACES_MAX; i++) {
|
||||
fbotex_init(&p->surfaces[i].fbotex, gl, p->log, w, h, p->gl_target,
|
||||
filter, p->opts.fbo_format);
|
||||
}
|
||||
}
|
||||
|
||||
recreate_osd(p);
|
||||
|
||||
p->need_reinit_rendering = false;
|
||||
@@ -1452,10 +1449,11 @@ static void uninit_rendering(struct gl_video *p)
|
||||
p->dither_texture = 0;
|
||||
|
||||
fbotex_uninit(&p->indirect_fbo);
|
||||
fbotex_uninit(&p->inter_fbo);
|
||||
|
||||
for (int i = 0; i < FBOSURFACES_MAX; i++)
|
||||
for (int i = 0; i < FBOSURFACES_MAX; i++) {
|
||||
fbotex_uninit(&p->surfaces[i].fbotex);
|
||||
p->surfaces[i].valid = false;
|
||||
}
|
||||
|
||||
fbotex_uninit(&p->scale_sep_fbo);
|
||||
}
|
||||
@@ -1732,54 +1730,12 @@ static size_t fbosurface_next(struct gl_video *p)
|
||||
return (p->surface_idx + 1) % FBOSURFACES_MAX;
|
||||
}
|
||||
|
||||
static void gl_video_interpolate_frame(struct gl_video *p,
|
||||
struct pass *chain,
|
||||
struct frame_timing *t)
|
||||
// Handle all of the frame passes upto and including upscaling, assuming
|
||||
// upscaling is not part of the final pass
|
||||
static void gl_video_upscale_frame(struct gl_video *p, struct pass *chain, struct fbotex *inter_fbo)
|
||||
{
|
||||
GL *gl = p->gl;
|
||||
double inter_coeff = 0.0;
|
||||
int64_t prev_pts = p->surfaces[fbosurface_next(p)].pts;
|
||||
p->is_interpolated = prev_pts < t->pts;
|
||||
|
||||
if (p->is_interpolated) {
|
||||
MP_STATS(p, "new-pts");
|
||||
// fbosurface 0 is already bound from the caller
|
||||
p->surfaces[p->surface_idx].pts = t->pts;
|
||||
p->surface_idx = fbosurface_next(p);
|
||||
gl->ActiveTexture(GL_TEXTURE0 + 1);
|
||||
gl->BindTexture(p->gl_target, p->surfaces[p->surface_idx].fbotex.texture);
|
||||
gl->ActiveTexture(GL_TEXTURE0);
|
||||
if (prev_pts < t->next_vsync && t->pts > t->next_vsync) {
|
||||
double N = t->next_vsync - t->prev_vsync;
|
||||
double P = t->pts - t->prev_vsync;
|
||||
float ts = p->opts.smoothmotion_threshold;
|
||||
inter_coeff = 1 - (N / P);
|
||||
inter_coeff = inter_coeff < 0.0 + ts ? 0.0 : inter_coeff;
|
||||
inter_coeff = inter_coeff > 1.0 - ts ? 1.0 : inter_coeff;
|
||||
MP_DBG(p, "inter frame ppts: %lld, pts: %lld, "
|
||||
"vsync: %lld, mix: %f\n",
|
||||
(long long)prev_pts, (long long)t->pts,
|
||||
(long long)t->next_vsync, inter_coeff);
|
||||
MP_STATS(p, "frame-mix");
|
||||
|
||||
// the value is scaled to fit in the graph with the completely
|
||||
// unrelated "phase" value (which is stupid)
|
||||
MP_STATS(p, "value-timed %lld %f mix-value",
|
||||
(long long)t->pts, inter_coeff * 10000);
|
||||
}
|
||||
}
|
||||
|
||||
gl->UseProgram(p->inter_program);
|
||||
GLint loc = gl->GetUniformLocation(p->inter_program, "inter_coeff");
|
||||
gl->Uniform1f(loc, inter_coeff);
|
||||
handle_pass(p, chain, &p->inter_fbo, p->inter_program);
|
||||
}
|
||||
|
||||
// (fbo==0 makes BindFramebuffer select the screen backbuffer)
|
||||
void gl_video_render_frame(struct gl_video *p, int fbo, struct frame_timing *t)
|
||||
{
|
||||
GL *gl = p->gl;
|
||||
struct video_image *vimg = &p->image;
|
||||
// Order of processing: [indirect -> [scale_sep ->]] inter
|
||||
handle_pass(p, chain, &p->indirect_fbo, p->indirect_program);
|
||||
|
||||
// compensated for optional rotation
|
||||
struct mp_rect src_rect_rot = p->src_rect;
|
||||
@@ -1788,6 +1744,99 @@ void gl_video_render_frame(struct gl_video *p, int fbo, struct frame_timing *t)
|
||||
MPSWAP(int, src_rect_rot.x1, src_rect_rot.y1);
|
||||
}
|
||||
|
||||
// Clip to visible height so that separate scaling scales the visible part
|
||||
// only (and the target FBO texture can have a bounded size).
|
||||
// Don't clamp width; too hard to get correct final scaling on l/r borders.
|
||||
chain->f.vp_y = src_rect_rot.y0;
|
||||
chain->f.vp_h = src_rect_rot.y1 - src_rect_rot.y0;
|
||||
|
||||
handle_pass(p, chain, &p->scale_sep_fbo, p->scale_sep_program);
|
||||
|
||||
// For Y direction, use the whole source viewport; it has been fit to the
|
||||
// correct origin/height before.
|
||||
// For X direction, assume the texture wasn't scaled yet, so we can
|
||||
// select the correct portion, which will be scaled to screen.
|
||||
chain->f.vp_x = src_rect_rot.x0;
|
||||
chain->f.vp_w = src_rect_rot.x1 - src_rect_rot.x0;
|
||||
|
||||
if (inter_fbo)
|
||||
handle_pass(p, chain, inter_fbo, p->inter_program);
|
||||
}
|
||||
|
||||
static double gl_video_interpolate_frame(struct gl_video *p,
|
||||
struct pass *chain,
|
||||
struct frame_timing *t)
|
||||
{
|
||||
GL *gl = p->gl;
|
||||
double inter_coeff = 0.0;
|
||||
int64_t prev_pts = p->surfaces[fbosurface_next(p)].pts;
|
||||
|
||||
// Make sure all surfaces are actually valid, and redraw them manually
|
||||
// if this is not the case
|
||||
for (int i = 0; i < FBOSURFACES_MAX; i++) {
|
||||
if (!p->surfaces[i].valid) {
|
||||
struct pass frame = { .f = chain->f };
|
||||
gl_video_upscale_frame(p, &frame, &p->surfaces[i].fbotex);
|
||||
p->surfaces[i].valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (t && prev_pts < t->pts) {
|
||||
MP_STATS(p, "new-pts");
|
||||
gl_video_upscale_frame(p, chain, &p->surfaces[p->surface_idx].fbotex);
|
||||
p->surfaces[p->surface_idx].valid = true;
|
||||
p->surfaces[p->surface_idx].pts = t->pts;
|
||||
p->surface_idx = fbosurface_next(p);
|
||||
} else {
|
||||
// re-use the previously rendered surface as source
|
||||
chain->f = p->surfaces[fbosurface_next(p)].fbotex;
|
||||
}
|
||||
|
||||
// fbosurface 0 is bound by handle_pass
|
||||
gl->ActiveTexture(GL_TEXTURE0 + 1);
|
||||
gl->BindTexture(p->gl_target, p->surfaces[p->surface_idx].fbotex.texture);
|
||||
gl->ActiveTexture(GL_TEXTURE0);
|
||||
|
||||
if (!t) {
|
||||
p->is_interpolated = false;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
int64_t vsync_interval = t->next_vsync - t->prev_vsync;
|
||||
|
||||
if (t->pts > t->next_vsync && t->pts < t->next_vsync + vsync_interval) {
|
||||
// current frame overlaps PTS boundary, blend
|
||||
double R = t->pts - t->next_vsync;
|
||||
float ts = p->opts.smoothmotion_threshold;
|
||||
inter_coeff = R / vsync_interval;
|
||||
inter_coeff = inter_coeff < 0.0 + ts ? 0.0 : inter_coeff;
|
||||
inter_coeff = inter_coeff > 1.0 - ts ? 1.0 : inter_coeff;
|
||||
MP_DBG(p, "inter frame ppts: %lld, pts: %lld, "
|
||||
"vsync: %lld, mix: %f\n",
|
||||
(long long)prev_pts, (long long)t->pts,
|
||||
(long long)t->next_vsync, inter_coeff);
|
||||
MP_STATS(p, "frame-mix");
|
||||
|
||||
// the value is scaled to fit in the graph with the completely
|
||||
// unrelated "phase" value (which is stupid)
|
||||
MP_STATS(p, "value-timed %lld %f mix-value",
|
||||
(long long)t->pts, inter_coeff * 10000);
|
||||
} else if (t->pts > t->next_vsync) {
|
||||
// there's a new frame, but we haven't displayed or blended it yet,
|
||||
// so we still draw the old frame
|
||||
inter_coeff = 1.0;
|
||||
}
|
||||
|
||||
p->is_interpolated = inter_coeff > 0.0;
|
||||
return inter_coeff;
|
||||
}
|
||||
|
||||
// (fbo==0 makes BindFramebuffer select the screen backbuffer)
|
||||
void gl_video_render_frame(struct gl_video *p, int fbo, struct frame_timing *t)
|
||||
{
|
||||
GL *gl = p->gl;
|
||||
struct video_image *vimg = &p->image;
|
||||
|
||||
p->is_interpolated = false;
|
||||
|
||||
gl->BindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
@@ -1808,9 +1857,6 @@ void gl_video_render_frame(struct gl_video *p, int fbo, struct frame_timing *t)
|
||||
goto draw_osd;
|
||||
}
|
||||
|
||||
// Order of processing:
|
||||
// [indirect -> [interpolate -> [scale_sep ->]]] final
|
||||
|
||||
GLuint imgtex[4] = {0};
|
||||
set_image_textures(p, vimg, imgtex);
|
||||
|
||||
@@ -1824,27 +1870,13 @@ void gl_video_render_frame(struct gl_video *p, int fbo, struct frame_timing *t)
|
||||
},
|
||||
};
|
||||
|
||||
int64_t prev_pts = p->surfaces[fbosurface_next(p)].pts;
|
||||
struct fbotex *indirect_target;
|
||||
if (p->inter_program && t && prev_pts < t->pts) {
|
||||
indirect_target = &p->surfaces[p->surface_idx].fbotex;
|
||||
double inter_coeff = 0.0;
|
||||
if (p->opts.smoothmotion) {
|
||||
inter_coeff = gl_video_interpolate_frame(p, &chain, t);
|
||||
} else {
|
||||
indirect_target = &p->indirect_fbo;
|
||||
gl_video_upscale_frame(p, &chain, NULL);
|
||||
}
|
||||
|
||||
handle_pass(p, &chain, indirect_target, p->indirect_program);
|
||||
|
||||
if (t && p->inter_program)
|
||||
gl_video_interpolate_frame(p, &chain, t);
|
||||
|
||||
// Clip to visible height so that separate scaling scales the visible part
|
||||
// only (and the target FBO texture can have a bounded size).
|
||||
// Don't clamp width; too hard to get correct final scaling on l/r borders.
|
||||
chain.f.vp_y = src_rect_rot.y0;
|
||||
chain.f.vp_h = src_rect_rot.y1 - src_rect_rot.y0;
|
||||
|
||||
handle_pass(p, &chain, &p->scale_sep_fbo, p->scale_sep_program);
|
||||
|
||||
struct fbotex screen = {
|
||||
.vp_x = p->vp_x,
|
||||
.vp_y = p->vp_y,
|
||||
@@ -1853,18 +1885,14 @@ void gl_video_render_frame(struct gl_video *p, int fbo, struct frame_timing *t)
|
||||
.fbo = fbo,
|
||||
};
|
||||
|
||||
// For Y direction, use the whole source viewport; it has been fit to the
|
||||
// correct origin/height before.
|
||||
// For X direction, assume the texture wasn't scaled yet, so we can
|
||||
// select the correct portion, which will be scaled to screen.
|
||||
chain.f.vp_x = src_rect_rot.x0;
|
||||
chain.f.vp_w = src_rect_rot.x1 - src_rect_rot.x0;
|
||||
|
||||
chain.use_dst = true;
|
||||
chain.dst = p->dst_rect;
|
||||
chain.flags = (p->image_params.rotate % 90 ? 0 : p->image_params.rotate / 90)
|
||||
| (vimg->image_flipped ? 4 : 0);
|
||||
|
||||
gl->UseProgram(p->final_program);
|
||||
GLint loc = gl->GetUniformLocation(p->final_program, "inter_coeff");
|
||||
gl->Uniform1f(loc, inter_coeff);
|
||||
handle_pass(p, &chain, &screen, p->final_program);
|
||||
|
||||
gl->UseProgram(0);
|
||||
@@ -1883,22 +1911,39 @@ draw_osd:
|
||||
|
||||
static void update_window_sized_objects(struct gl_video *p)
|
||||
{
|
||||
int w = p->dst_rect.x1 - p->dst_rect.x0;
|
||||
int h = p->dst_rect.y1 - p->dst_rect.y0;
|
||||
if ((p->image_params.rotate % 180) == 90)
|
||||
MPSWAP(int, w, h);
|
||||
|
||||
// Round up to an arbitrary alignment to make window resizing or
|
||||
// panscan controls smoother (less texture reallocations).
|
||||
int width = FFALIGN(w, 256);
|
||||
int height = FFALIGN(h, 256);
|
||||
|
||||
if (p->scale_sep_program) {
|
||||
int w = p->dst_rect.x1 - p->dst_rect.x0;
|
||||
int h = p->dst_rect.y1 - p->dst_rect.y0;
|
||||
if ((p->image_params.rotate % 180) == 90)
|
||||
MPSWAP(int, w, h);
|
||||
if (h > p->scale_sep_fbo.tex_h) {
|
||||
fbotex_uninit(&p->scale_sep_fbo);
|
||||
// Round up to an arbitrary alignment to make window resizing or
|
||||
// panscan controls smoother (less texture reallocations).
|
||||
int height = FFALIGN(h, 256);
|
||||
fbotex_init(&p->scale_sep_fbo, p->gl, p->log, p->image_w, height,
|
||||
p->gl_target, GL_NEAREST, p->opts.fbo_format);
|
||||
}
|
||||
p->scale_sep_fbo.vp_w = p->image_w;
|
||||
p->scale_sep_fbo.vp_h = h;
|
||||
}
|
||||
|
||||
if (p->opts.smoothmotion) {
|
||||
for (int i = 0; i < FBOSURFACES_MAX; i++) {
|
||||
struct fbotex *fbo = &p->surfaces[i].fbotex;
|
||||
if (w > fbo->tex_w || h > fbo->tex_h) {
|
||||
fbotex_uninit(fbo);
|
||||
fbotex_init(fbo, p->gl, p->log, width, height,
|
||||
p->gl_target, GL_NEAREST, p->opts.fbo_format);
|
||||
}
|
||||
fbo->vp_w = w;
|
||||
fbo->vp_h = h;
|
||||
p->surfaces[i].valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void check_resize(struct gl_video *p)
|
||||
@@ -2127,6 +2172,10 @@ static void check_gl_features(struct gl_video *p)
|
||||
p->use_lut_3d = false;
|
||||
disabled[n_disabled++] = "color management (FBO)";
|
||||
}
|
||||
if (!have_fbo && p->opts.smoothmotion) {
|
||||
p->opts.smoothmotion = false;
|
||||
disabled[n_disabled++] = "smoothmotion (FBO)";
|
||||
}
|
||||
// because of bt709_expand()
|
||||
if (!have_mix && p->use_lut_3d) {
|
||||
p->use_lut_3d = false;
|
||||
|
||||
@@ -252,7 +252,6 @@ float[6] weights6(sampler2D lookup, float f) {
|
||||
vec4 c2 = texture(lookup, vec2(0.75, f));
|
||||
return float[6](c1.r, c1.g, c1.b, c2.r, c2.g, c2.b);
|
||||
}
|
||||
#endif
|
||||
|
||||
// For N=n*4 with n>1.
|
||||
#define WEIGHTS_N(NAME, N) \
|
||||
@@ -305,12 +304,12 @@ float[6] weights6(sampler2D lookup, float f) {
|
||||
w = texture1D(LUT, length(vec2(X, Y) - fcoord)/R).r; \
|
||||
c = texture(tex, base + pt * vec2(X, Y)); \
|
||||
wsum += w; \
|
||||
res += vec4(w) * c; \
|
||||
res += vec4(w) * c;
|
||||
|
||||
#define SAMPLE_POLAR_PRIMARY(LUT, R, X, Y) \
|
||||
SAMPLE_POLAR_HELPER(LUT, R, X, Y) \
|
||||
lo = min(lo, c); \
|
||||
hi = max(hi, c); \
|
||||
hi = max(hi, c);
|
||||
|
||||
#define SAMPLE_CONVOLUTION_POLAR_R(NAME, R, LUT, WEIGHTS_FN, ANTIRING) \
|
||||
vec4 NAME(VIDEO_SAMPLER tex, vec2 texsize, vec2 texcoord) { \
|
||||
@@ -328,6 +327,8 @@ float[6] weights6(sampler2D lookup, float f) {
|
||||
return mix(res, clamp(res, lo, hi), ANTIRING); \
|
||||
}
|
||||
|
||||
#endif /* HAVE_ARRAYS */
|
||||
|
||||
#ifdef DEF_SCALER0
|
||||
DEF_SCALER0
|
||||
#endif
|
||||
@@ -387,9 +388,6 @@ void main() {
|
||||
texture(texture0, texcoord),
|
||||
texture(texture1, texcoord),
|
||||
inter_coeff);
|
||||
// debug code to visually check the interpolation amount
|
||||
// vec4 acolor = texture(texture0, texcoord) -
|
||||
// inter_coeff * texture(texture1, texcoord);
|
||||
#elif USE_CONV == CONV_PLANAR
|
||||
vec4 acolor = vec4(SAMPLE(texture0, textures_size[0], texcoord).r,
|
||||
SAMPLE_C(texture1, textures_size[1], chr_texcoord).r,
|
||||
|
||||
@@ -106,7 +106,7 @@ def check_cocoa(ctx, dependency_identifier):
|
||||
fn = check_cc(
|
||||
fragment = load_fragment('cocoa.m'),
|
||||
compile_filename = 'test.m',
|
||||
framework_name = ['Cocoa', 'IOKit', 'OpenGL'],
|
||||
framework_name = ['Cocoa', 'IOKit', 'OpenGL', 'QuartzCore'],
|
||||
includes = ctx.srcnode.abspath(),
|
||||
linkflags = '-fobjc-arc')
|
||||
|
||||
|
||||
@@ -103,6 +103,8 @@ def check_pkg_config(*args, **kw_ext):
|
||||
if result:
|
||||
ctx.define(defkey, 1)
|
||||
else:
|
||||
ctx.add_optional_message(dependency_identifier,
|
||||
"'{0}' not found".format(" ".join(sargs)))
|
||||
ctx.undefine(defkey)
|
||||
return result
|
||||
return fn
|
||||
|
||||
@@ -9,5 +9,7 @@ int main(int argc, char **argv) {
|
||||
NSArray *ary = @[@1, @2, @3];
|
||||
NSLog(@"test subscripting: %@", ary[0]);
|
||||
NSApplicationLoad();
|
||||
CVDisplayLinkRef link;
|
||||
CVDisplayLinkCreateWithCGDisplay(0, &link);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,13 @@ def build(ctx):
|
||||
|
||||
cls = Task.classes['cprogram']
|
||||
class cprogram(cls):
|
||||
run_str = cls.hcode + '${LAST_LINKFLAGS}'
|
||||
try:
|
||||
run_str = cls.orig_run_str + '${LAST_LINKFLAGS}'
|
||||
except AttributeError:
|
||||
try:
|
||||
run_str = cls.hcode + '${LAST_LINKFLAGS}'
|
||||
except TypeError:
|
||||
run_str = cls.hcode.decode('iso8859-1') + '${LAST_LINKFLAGS}'
|
||||
|
||||
cls = Task.classes['macplist']
|
||||
class macplist(cls):
|
||||
|
||||
@@ -705,9 +705,7 @@ hwaccel_features = [
|
||||
'name': '--vda-gl',
|
||||
'desc': 'VDA with OpenGL',
|
||||
'deps': [ 'gl-cocoa', 'vda-hwaccel' ],
|
||||
# apparently a bug in waf causes msg= to be needed when passing only
|
||||
# framework= (it probably fails to infer it)
|
||||
'func': check_cc(msg='QuartzCore', framework='QuartzCore')
|
||||
'func': check_true
|
||||
}, {
|
||||
'name': '--vdpau-hwaccel',
|
||||
'desc': 'libavcodec VDPAU hwaccel',
|
||||
|
||||
+2
-3
@@ -172,12 +172,14 @@ def build(ctx):
|
||||
( "demux/demux_libass.c", "libass"),
|
||||
( "demux/demux_mf.c" ),
|
||||
( "demux/demux_mkv.c" ),
|
||||
( "demux/demux_mkv_timeline.c" ),
|
||||
( "demux/demux_playlist.c" ),
|
||||
( "demux/demux_raw.c" ),
|
||||
( "demux/demux_subreader.c" ),
|
||||
( "demux/demux_tv.c", "tv" ),
|
||||
( "demux/ebml.c" ),
|
||||
( "demux/packet.c" ),
|
||||
( "demux/timeline.c" ),
|
||||
|
||||
## Input
|
||||
( "input/cmd_list.c" ),
|
||||
@@ -222,9 +224,6 @@ def build(ctx):
|
||||
( "player/screenshot.c" ),
|
||||
( "player/scripting.c" ),
|
||||
( "player/sub.c" ),
|
||||
( "player/timeline/tl_cue.c" ),
|
||||
( "player/timeline/tl_mpv_edl.c" ),
|
||||
( "player/timeline/tl_matroska.c" ),
|
||||
( "player/video.c" ),
|
||||
|
||||
## Streams
|
||||
|
||||
Reference in new issue
Block a user