mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-05 21:48:31 +01:00
Add PIP support for syncplay
This commit is contained in:
@@ -50,6 +50,13 @@ class MpvPlayerModule : Module() {
|
||||
// No-op on Android - media session integration would require MediaSessionCompat
|
||||
}
|
||||
|
||||
// When true, PiP play/pause/skip controls emit JS events
|
||||
// instead of driving MPV directly, so the host app can route
|
||||
// through SyncPlay (server -> group broadcast -> all clients).
|
||||
Prop("syncPlayDelegated") { view: MpvPlayerView, delegated: Boolean ->
|
||||
view.syncPlayDelegated = delegated
|
||||
}
|
||||
|
||||
// Async function to play video
|
||||
AsyncFunction("play") { view: MpvPlayerView ->
|
||||
view.play()
|
||||
@@ -198,7 +205,7 @@ class MpvPlayerModule : Module() {
|
||||
}
|
||||
|
||||
// Defines events that the view can send to JavaScript
|
||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange")
|
||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange", "onPipPlayRequest", "onPipPauseRequest", "onPipSkipRequest")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,16 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
val onError by EventDispatcher()
|
||||
val onTracksReady by EventDispatcher()
|
||||
val onPictureInPictureChange by EventDispatcher()
|
||||
// SyncPlay: when `syncPlayDelegated == true`, PiP playback controls
|
||||
// (play / pause / skip) emit these events instead of driving MPV
|
||||
// directly, so JS can route the action through the SyncPlay
|
||||
// controller (server -> group broadcast -> all clients). Default
|
||||
// behavior (non-SyncPlay) is unchanged.
|
||||
val onPipPlayRequest by EventDispatcher()
|
||||
val onPipPauseRequest by EventDispatcher()
|
||||
val onPipSkipRequest by EventDispatcher()
|
||||
|
||||
var syncPlayDelegated: Boolean = false
|
||||
|
||||
private var textureView: TextureView
|
||||
private var renderer: MPVLayerRenderer? = null
|
||||
@@ -85,14 +95,32 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
pipController?.setPlayerView(textureView)
|
||||
pipController?.delegate = object : PiPController.Delegate {
|
||||
override fun onPlay() {
|
||||
if (syncPlayDelegated) {
|
||||
onPipPlayRequest(mapOf<String, Any>())
|
||||
return
|
||||
}
|
||||
play()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (syncPlayDelegated) {
|
||||
onPipPauseRequest(mapOf<String, Any>())
|
||||
return
|
||||
}
|
||||
pause()
|
||||
}
|
||||
|
||||
override fun onSeekBy(seconds: Double) {
|
||||
if (syncPlayDelegated) {
|
||||
val target = (cachedPosition + seconds).coerceAtLeast(0.0)
|
||||
onPipSkipRequest(
|
||||
mapOf(
|
||||
"targetSeconds" to target,
|
||||
"intervalSeconds" to seconds
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
seekBy(seconds)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,13 @@ public class MpvPlayerModule: Module {
|
||||
}
|
||||
}
|
||||
|
||||
// When true, PiP play/pause/skip controls emit JS events instead
|
||||
// of driving MPV directly, so the host app can route through
|
||||
// SyncPlay (server -> group broadcast -> all clients).
|
||||
Prop("syncPlayDelegated") { (view: MpvPlayerView, delegated: Bool) in
|
||||
view.syncPlayDelegated = delegated
|
||||
}
|
||||
|
||||
// Async function to play video
|
||||
AsyncFunction("play") { (view: MpvPlayerView) in
|
||||
view.play()
|
||||
@@ -213,7 +220,7 @@ public class MpvPlayerModule: Module {
|
||||
}
|
||||
|
||||
// Defines events that the view can send to JavaScript
|
||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange")
|
||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange", "onPipPlayRequest", "onPipPauseRequest", "onPipSkipRequest")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,16 @@ class MpvPlayerView: ExpoView {
|
||||
let onError = EventDispatcher()
|
||||
let onTracksReady = EventDispatcher()
|
||||
let onPictureInPictureChange = EventDispatcher()
|
||||
// SyncPlay: when `syncPlayDelegated == true`, PiP playback controls
|
||||
// (play / pause / skip) emit these events instead of driving MPV
|
||||
// directly, so JS can route the action through the SyncPlay
|
||||
// controller (server → group broadcast → all clients). Default
|
||||
// behavior (non-SyncPlay) is unchanged.
|
||||
let onPipPlayRequest = EventDispatcher()
|
||||
let onPipPauseRequest = EventDispatcher()
|
||||
let onPipSkipRequest = EventDispatcher()
|
||||
|
||||
var syncPlayDelegated: Bool = false
|
||||
|
||||
private var currentURL: URL?
|
||||
private var cachedPosition: Double = 0
|
||||
@@ -77,7 +87,6 @@ class MpvPlayerView: ExpoView {
|
||||
super.init(appContext: appContext)
|
||||
setupNotifications()
|
||||
setupView()
|
||||
// Note: Decoder reset is handled automatically via KVO in MPVLayerRenderer
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
@@ -672,6 +681,12 @@ extension MpvPlayerView: PiPControllerDelegate {
|
||||
|
||||
func pipControllerPlay(_ controller: PiPController) {
|
||||
print("PiP play requested")
|
||||
if syncPlayDelegated {
|
||||
// Let JS route through SyncPlay. We deliberately do NOT touch
|
||||
// MPV here; the WS command coming back will drive playback.
|
||||
onPipPlayRequest([:])
|
||||
return
|
||||
}
|
||||
intendedPlayState = true
|
||||
renderer?.play()
|
||||
pipController?.setPlaybackRate(1.0)
|
||||
@@ -679,6 +694,10 @@ extension MpvPlayerView: PiPControllerDelegate {
|
||||
|
||||
func pipControllerPause(_ controller: PiPController) {
|
||||
print("PiP pause requested")
|
||||
if syncPlayDelegated {
|
||||
onPipPauseRequest([:])
|
||||
return
|
||||
}
|
||||
intendedPlayState = false
|
||||
renderer?.pausePlayback()
|
||||
pipController?.setPlaybackRate(0.0)
|
||||
@@ -688,6 +707,16 @@ extension MpvPlayerView: PiPControllerDelegate {
|
||||
let seconds = CMTimeGetSeconds(interval)
|
||||
print("PiP skip by interval: \(seconds)")
|
||||
let target = max(0, cachedPosition + seconds)
|
||||
if syncPlayDelegated {
|
||||
// `targetSeconds` lets JS convert to ticks and call
|
||||
// syncPlayController.seek(). `intervalSeconds` is also sent
|
||||
// for telemetry / debug.
|
||||
onPipSkipRequest([
|
||||
"targetSeconds": target,
|
||||
"intervalSeconds": seconds
|
||||
])
|
||||
return
|
||||
}
|
||||
seekTo(position: target)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@ export type OnPictureInPictureChangePayload = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Emitted when the user taps a PiP playback control while the view
|
||||
* was rendered with `syncPlayDelegated`. The host app should route
|
||||
* the action through the SyncPlay controller instead of acting
|
||||
* locally.
|
||||
*/
|
||||
export type OnPipPlayRequestPayload = Record<string, never>;
|
||||
export type OnPipPauseRequestPayload = Record<string, never>;
|
||||
export type OnPipSkipRequestPayload = {
|
||||
/** Absolute target position the user wants to seek to, in seconds. */
|
||||
targetSeconds: number;
|
||||
/** Skip interval requested by the OS (signed seconds). Debug only. */
|
||||
intervalSeconds: number;
|
||||
};
|
||||
|
||||
export type NowPlayingMetadata = {
|
||||
title?: string;
|
||||
artist?: string;
|
||||
@@ -84,6 +99,18 @@ export type MpvPlayerViewProps = {
|
||||
onPictureInPictureChange?: (event: {
|
||||
nativeEvent: OnPictureInPictureChangePayload;
|
||||
}) => void;
|
||||
/**
|
||||
* When true, PiP play/pause/skip controls emit the corresponding
|
||||
* `onPipPlayRequest` / `onPipPauseRequest` / `onPipSkipRequest`
|
||||
* events instead of driving MPV directly. Used to route PiP control
|
||||
* actions through SyncPlay.
|
||||
*/
|
||||
syncPlayDelegated?: boolean;
|
||||
onPipPlayRequest?: (event: { nativeEvent: OnPipPlayRequestPayload }) => void;
|
||||
onPipPauseRequest?: (event: {
|
||||
nativeEvent: OnPipPauseRequestPayload;
|
||||
}) => void;
|
||||
onPipSkipRequest?: (event: { nativeEvent: OnPipSkipRequestPayload }) => void;
|
||||
};
|
||||
|
||||
export interface MpvPlayerViewRef {
|
||||
|
||||
Reference in New Issue
Block a user