mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
fix(playback): detect embedded mpv keep-open eof (#1050)
* fix(playback): detect embedded mpv keep-open eof * fix(playback): keep mpv redirects loading
This commit is contained in:
1 parent
e8aa7c3a34
commit
83db27ef12
4 files changed
+164
-17
No files matched your search
@@ -1136,6 +1136,13 @@ void runEventLoop(const std::shared_ptr<Session>& session)
|
||||
endFile->reason == MPV_END_FILE_REASON_EOF &&
|
||||
session->running.load()) {
|
||||
session->snapshot.status = SessionStatus::Ended;
|
||||
} else if (
|
||||
endFile &&
|
||||
endFile->reason == MPV_END_FILE_REASON_REDIRECT &&
|
||||
session->running.load()) {
|
||||
session->snapshot.status = SessionStatus::Loading;
|
||||
session->snapshot.error.clear();
|
||||
session->loadedPath = false;
|
||||
} else if (session->running.load()) {
|
||||
session->snapshot.status = SessionStatus::Idle;
|
||||
}
|
||||
@@ -1181,6 +1188,19 @@ void runEventLoop(const std::shared_ptr<Session>& session)
|
||||
break;
|
||||
}
|
||||
|
||||
if (propertyName == "eof-reached" &&
|
||||
property->format == MPV_FORMAT_FLAG &&
|
||||
property->data) {
|
||||
const bool eofReached =
|
||||
*static_cast<int*>(property->data) != 0;
|
||||
if (eofReached &&
|
||||
session->running.load() &&
|
||||
session->loadedPath) {
|
||||
session->snapshot.status = SessionStatus::Ended;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (propertyName == "volume" &&
|
||||
property->format == MPV_FORMAT_DOUBLE &&
|
||||
property->data) {
|
||||
@@ -1694,6 +1714,7 @@ Napi::Value CreateSession(const Napi::CallbackInfo& info)
|
||||
"video-aspect-override",
|
||||
MPV_FORMAT_STRING
|
||||
);
|
||||
mpv_observe_property(session->handle, 11, "eof-reached", MPV_FORMAT_FLAG);
|
||||
|
||||
session->running.store(true);
|
||||
session->eventThread = std::thread(runEventLoop, session);
|
||||
|
||||
@@ -31,6 +31,7 @@ enum class SessionStatus {
|
||||
Loading,
|
||||
Playing,
|
||||
Paused,
|
||||
Ended,
|
||||
Error,
|
||||
Closed,
|
||||
};
|
||||
@@ -95,6 +96,8 @@ std::string toStatusString(SessionStatus status)
|
||||
return "playing";
|
||||
case SessionStatus::Paused:
|
||||
return "paused";
|
||||
case SessionStatus::Ended:
|
||||
return "ended";
|
||||
case SessionStatus::Error:
|
||||
return "error";
|
||||
case SessionStatus::Closed:
|
||||
@@ -518,6 +521,19 @@ void runEventLoop(std::shared_ptr<Session> session)
|
||||
session->snapshot.error = endFile->error < 0
|
||||
? mpv_error_string(endFile->error)
|
||||
: "Embedded MPV playback ended with an error.";
|
||||
} else if (
|
||||
endFile &&
|
||||
endFile->reason == MPV_END_FILE_REASON_EOF &&
|
||||
session->running.load()) {
|
||||
session->snapshot.status = SessionStatus::Ended;
|
||||
} else if (
|
||||
endFile &&
|
||||
endFile->reason == MPV_END_FILE_REASON_REDIRECT &&
|
||||
session->running.load()) {
|
||||
session->snapshot.status = SessionStatus::Loading;
|
||||
session->snapshot.error.clear();
|
||||
} else if (session->running.load()) {
|
||||
session->snapshot.status = SessionStatus::Idle;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -536,10 +552,22 @@ void runEventLoop(std::shared_ptr<Session> session)
|
||||
*static_cast<double*>(property->data);
|
||||
} else if (name == "pause" && property->format == MPV_FORMAT_FLAG) {
|
||||
const bool paused = *static_cast<int*>(property->data) != 0;
|
||||
session->snapshot.status = paused
|
||||
? SessionStatus::Paused
|
||||
: SessionStatus::Playing;
|
||||
session->snapshot.error.clear();
|
||||
if (
|
||||
session->snapshot.status != SessionStatus::Loading &&
|
||||
session->snapshot.status != SessionStatus::Ended &&
|
||||
session->snapshot.status != SessionStatus::Error
|
||||
) {
|
||||
session->snapshot.status = paused
|
||||
? SessionStatus::Paused
|
||||
: SessionStatus::Playing;
|
||||
session->snapshot.error.clear();
|
||||
}
|
||||
} else if (name == "eof-reached" && property->format == MPV_FORMAT_FLAG) {
|
||||
const bool eofReached =
|
||||
*static_cast<int*>(property->data) != 0;
|
||||
if (eofReached && session->running.load()) {
|
||||
session->snapshot.status = SessionStatus::Ended;
|
||||
}
|
||||
} else if (name == "volume" && property->format == MPV_FORMAT_DOUBLE) {
|
||||
session->snapshot.volumePercent =
|
||||
*static_cast<double*>(property->data);
|
||||
@@ -748,6 +776,7 @@ Napi::Value CreateSession(const Napi::CallbackInfo& info)
|
||||
"video-aspect-override",
|
||||
MPV_FORMAT_STRING
|
||||
);
|
||||
mpv_observe_property(session->handle, 11, "eof-reached", MPV_FORMAT_FLAG);
|
||||
|
||||
session->running.store(true);
|
||||
session->eventThread = std::thread(runEventLoop, session);
|
||||
|
||||
@@ -53,6 +53,27 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
throw new Error(`Unable to read ${name} body.`);
|
||||
}
|
||||
|
||||
function eventCase(
|
||||
source: string,
|
||||
eventName: string,
|
||||
nextEventName: string
|
||||
): string {
|
||||
const eventLoopBodyStart = source.indexOf('void runEventLoop(');
|
||||
expect(eventLoopBodyStart).toBeGreaterThanOrEqual(0);
|
||||
const eventCaseStart = source.indexOf(
|
||||
`case ${eventName}`,
|
||||
eventLoopBodyStart
|
||||
);
|
||||
expect(eventCaseStart).toBeGreaterThanOrEqual(0);
|
||||
const nextCaseStart = source.indexOf(
|
||||
`case ${nextEventName}`,
|
||||
eventCaseStart
|
||||
);
|
||||
expect(nextCaseStart).toBeGreaterThan(eventCaseStart);
|
||||
|
||||
return source.slice(eventCaseStart, nextCaseStart);
|
||||
}
|
||||
|
||||
it('tracks LoadPlayback recording auto-stop replies through the reconciler', () => {
|
||||
const body = functionBody('LoadPlayback');
|
||||
|
||||
@@ -72,23 +93,96 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
it('maps successful MPV end-file events to an ended session status', () => {
|
||||
expect(nativeSource).toContain('Ended,');
|
||||
expect(nativeSource).toContain('case SessionStatus::Ended:');
|
||||
expect(widCommonSource).toContain('Ended,');
|
||||
expect(widCommonSource).toContain('case SessionStatus::Ended:');
|
||||
|
||||
const eventLoopBodyStart = nativeSource.indexOf('void runEventLoop(');
|
||||
expect(eventLoopBodyStart).toBeGreaterThanOrEqual(0);
|
||||
const endFileCaseStart = nativeSource.indexOf(
|
||||
'case MPV_EVENT_END_FILE',
|
||||
eventLoopBodyStart
|
||||
const endFileCase = eventCase(
|
||||
nativeSource,
|
||||
'MPV_EVENT_END_FILE',
|
||||
'MPV_EVENT_PROPERTY_CHANGE'
|
||||
);
|
||||
expect(endFileCaseStart).toBeGreaterThanOrEqual(0);
|
||||
const nextCaseStart = nativeSource.indexOf(
|
||||
'case MPV_EVENT_PROPERTY_CHANGE',
|
||||
endFileCaseStart
|
||||
);
|
||||
expect(nextCaseStart).toBeGreaterThan(endFileCaseStart);
|
||||
|
||||
const endFileCase = nativeSource.slice(endFileCaseStart, nextCaseStart);
|
||||
expect(endFileCase).toContain('SessionStatus::Ended');
|
||||
expect(endFileCase).toContain('MPV_END_FILE_REASON_EOF');
|
||||
|
||||
const widEndFileCase = eventCase(
|
||||
widCommonSource,
|
||||
'MPV_EVENT_END_FILE',
|
||||
'MPV_EVENT_PROPERTY_CHANGE'
|
||||
);
|
||||
expect(widEndFileCase).toContain('SessionStatus::Ended');
|
||||
expect(widEndFileCase).toContain('MPV_END_FILE_REASON_EOF');
|
||||
});
|
||||
|
||||
it('keeps MPV redirect end-file events in loading state', () => {
|
||||
for (const endFileCase of [
|
||||
eventCase(
|
||||
nativeSource,
|
||||
'MPV_EVENT_END_FILE',
|
||||
'MPV_EVENT_PROPERTY_CHANGE'
|
||||
),
|
||||
eventCase(
|
||||
widCommonSource,
|
||||
'MPV_EVENT_END_FILE',
|
||||
'MPV_EVENT_PROPERTY_CHANGE'
|
||||
),
|
||||
]) {
|
||||
const redirectBranchStart = endFileCase.indexOf(
|
||||
'MPV_END_FILE_REASON_REDIRECT'
|
||||
);
|
||||
expect(redirectBranchStart).toBeGreaterThanOrEqual(0);
|
||||
const idleFallbackStart = endFileCase.indexOf(
|
||||
'SessionStatus::Idle',
|
||||
redirectBranchStart
|
||||
);
|
||||
expect(idleFallbackStart).toBeGreaterThan(redirectBranchStart);
|
||||
|
||||
const redirectBranch = endFileCase.slice(
|
||||
redirectBranchStart,
|
||||
idleFallbackStart
|
||||
);
|
||||
expect(redirectBranch).toContain('SessionStatus::Loading');
|
||||
expect(redirectBranch).toContain('session->snapshot.error.clear();');
|
||||
}
|
||||
});
|
||||
|
||||
it('maps keep-open eof-reached property changes to an ended session status', () => {
|
||||
expect(nativeSource).toContain(
|
||||
'mpv_observe_property(session->handle, 11, "eof-reached", MPV_FORMAT_FLAG);'
|
||||
);
|
||||
expect(widCommonSource).toContain(
|
||||
'mpv_observe_property(session->handle, 11, "eof-reached", MPV_FORMAT_FLAG);'
|
||||
);
|
||||
|
||||
const nativeEofBranchStart = nativeSource.indexOf(
|
||||
'propertyName == "eof-reached"'
|
||||
);
|
||||
expect(nativeEofBranchStart).toBeGreaterThanOrEqual(0);
|
||||
const nativeVolumeBranchStart = nativeSource.indexOf(
|
||||
'propertyName == "volume"',
|
||||
nativeEofBranchStart
|
||||
);
|
||||
expect(nativeVolumeBranchStart).toBeGreaterThan(nativeEofBranchStart);
|
||||
const nativeEofBranch = nativeSource.slice(
|
||||
nativeEofBranchStart,
|
||||
nativeVolumeBranchStart
|
||||
);
|
||||
expect(nativeEofBranch).toContain('SessionStatus::Ended');
|
||||
expect(nativeEofBranch).toContain('session->loadedPath');
|
||||
|
||||
const widEofBranchStart = widCommonSource.indexOf(
|
||||
'name == "eof-reached"'
|
||||
);
|
||||
expect(widEofBranchStart).toBeGreaterThanOrEqual(0);
|
||||
const widVolumeBranchStart = widCommonSource.indexOf(
|
||||
'name == "volume"',
|
||||
widEofBranchStart
|
||||
);
|
||||
expect(widVolumeBranchStart).toBeGreaterThan(widEofBranchStart);
|
||||
const widEofBranch = widCommonSource.slice(
|
||||
widEofBranchStart,
|
||||
widVolumeBranchStart
|
||||
);
|
||||
expect(widEofBranch).toContain('SessionStatus::Ended');
|
||||
});
|
||||
|
||||
it('keeps Windows/Linux non-load async MPV command failures non-fatal', () => {
|
||||
|
||||
@@ -82,11 +82,14 @@ The renderer learns which features the loaded addon binary supports through the
|
||||
|
||||
- `ended` when the end-file reason is `MPV_END_FILE_REASON_EOF`
|
||||
- `error` when MPV reports an end-file error
|
||||
- `loading` when MPV reports `MPV_END_FILE_REASON_REDIRECT`, because playback continues with the redirected playlist contents
|
||||
- `idle` for other successful end-file reasons such as replacement/stop
|
||||
- `closed` only for dispose/manual teardown
|
||||
|
||||
Renderer autoplay must use `ended` only. It must not treat `closed`, `idle`, or `error` as a request to continue to the next episode.
|
||||
|
||||
The native addon also observes mpv's `eof-reached` property and maps a true value to `ended`. This is required because embedded sessions run with `keep-open=yes`; MPV can pause at EOF while keeping the file loaded, so relying only on `MPV_EVENT_END_FILE` can leave the renderer in a paused-at-end state and block series autoplay.
|
||||
|
||||
Series episode navigation is owned by the portal feature components and passed through the shared inline player to `EmbeddedMpvPlayerComponent`. The embedded MPV controls show `skip_previous` and `skip_next` buttons only as part of the embedded player control surface. The shared navigation payload contains `canPrevious`, `canNext`, and `autoplayEnabled`; the component disables previous/next at the current-season boundaries and guards the output handlers as well as the button disabled state.
|
||||
|
||||
Autoplay is enabled by default for series playback in embedded MPV. On `ended`, Xtream and Stalker series detail views start the next episode only when the current episode has a next item in the same season. Playback stops on the last episode of the current season. Previous always switches to the previous episode in the current season; it does not implement a restart-threshold behavior.
|
||||
|
||||
Reference in new issue
Block a user