mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-07 11:52:34 +00:00
feat: player controls with track list signature and update logic
This commit is contained in:
parent
56885309ae
commit
22ae97f892
4 changed files with 234 additions and 43 deletions
|
|
@ -252,6 +252,8 @@ val buildMacosPlayerBridge = tasks.register<Exec>("buildMacosPlayerBridge") {
|
|||
"-framework",
|
||||
"QuartzCore",
|
||||
"-framework",
|
||||
"CoreVideo",
|
||||
"-framework",
|
||||
"OpenGL",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ internal class NativePlayerController(
|
|||
private var handle: Long = 0L
|
||||
private var pendingSource: PendingSource? = null
|
||||
private var controlsState = PlayerControlsState()
|
||||
private var lastSentControlsStructureKey: PlayerControlsState? = null
|
||||
private var onAction: (PlayerControlsAction) -> Boolean = { false }
|
||||
private var onEvent: (String, Double) -> Boolean = { _, _ -> false }
|
||||
private var onScrubChange: (Long) -> Boolean = { false }
|
||||
|
|
@ -100,9 +101,11 @@ internal class NativePlayerController(
|
|||
|
||||
fun updateControls(state: PlayerControlsState) {
|
||||
controlsState = state
|
||||
handle.takeIf { it != 0L }?.let { current ->
|
||||
NativePlayerBridge.updateControls(current, state.toControlsJson())
|
||||
}
|
||||
val current = handle.takeIf { it != 0L } ?: return
|
||||
val structureKey = state.nativeControlsStructureKey()
|
||||
if (structureKey == lastSentControlsStructureKey) return
|
||||
lastSentControlsStructureKey = structureKey
|
||||
NativePlayerBridge.updateControls(current, state.toControlsJson())
|
||||
}
|
||||
|
||||
fun setResizeMode(mode: PlayerResizeMode) {
|
||||
|
|
@ -191,6 +194,7 @@ internal class NativePlayerController(
|
|||
fun dispose() {
|
||||
val current = handle
|
||||
handle = 0L
|
||||
lastSentControlsStructureKey = null
|
||||
if (current != 0L) {
|
||||
runCatching { NativePlayerBridge.dispose(current) }
|
||||
}
|
||||
|
|
@ -631,6 +635,14 @@ private fun PlayerControlsState.toControlsJson(): String =
|
|||
append('}')
|
||||
}
|
||||
|
||||
private fun PlayerControlsState.nativeControlsStructureKey(): PlayerControlsState =
|
||||
copy(
|
||||
isPlaying = false,
|
||||
isLoading = false,
|
||||
durationMs = 0L,
|
||||
positionMs = 0L,
|
||||
)
|
||||
|
||||
private fun StringBuilder.appendJsonField(name: String, value: String) {
|
||||
append('"').append(name).append("\":")
|
||||
append(value.toJsonString())
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
#import <Cocoa/Cocoa.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
#import <OpenGL/OpenGL.h>
|
||||
#import <OpenGL/gl3.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <dlfcn.h>
|
||||
#include <string>
|
||||
|
|
@ -88,6 +91,8 @@ uint64_t mpv_render_context_update(mpv_render_context *ctx);
|
|||
@interface PlayerOpenGLView : NSOpenGLView
|
||||
@property(nonatomic, assign) mpv_render_context *renderContext;
|
||||
- (void)requestRender;
|
||||
- (void)stopRendering;
|
||||
- (void)clearRenderContext;
|
||||
@end
|
||||
|
||||
@interface PlayerScriptHandler : NSObject <WKScriptMessageHandler>
|
||||
|
|
@ -143,14 +148,29 @@ static void *getOpenGLProcAddress(void * /* ctx */, const char *name) {
|
|||
return openGlHandle ? dlsym(openGlHandle, name) : nullptr;
|
||||
}
|
||||
|
||||
static CVReturn displayLinkCallback(
|
||||
CVDisplayLinkRef /* displayLink */,
|
||||
const CVTimeStamp * /* now */,
|
||||
const CVTimeStamp * /* outputTime */,
|
||||
CVOptionFlags /* flagsIn */,
|
||||
CVOptionFlags * /* flagsOut */,
|
||||
void *displayLinkContext
|
||||
);
|
||||
|
||||
static void renderUpdateCallback(void *callbackContext) {
|
||||
PlayerOpenGLView *view = (__bridge PlayerOpenGLView *)callbackContext;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[view requestRender];
|
||||
});
|
||||
[view requestRender];
|
||||
}
|
||||
|
||||
@implementation PlayerOpenGLView
|
||||
@implementation PlayerOpenGLView {
|
||||
CVDisplayLinkRef _displayLink;
|
||||
CGLContextObj _cglContext;
|
||||
std::atomic_bool _renderRequested;
|
||||
std::atomic_bool _drawableReady;
|
||||
std::atomic_int _backingWidth;
|
||||
std::atomic_int _backingHeight;
|
||||
mpv_render_context *_renderContext;
|
||||
}
|
||||
|
||||
+ (NSOpenGLPixelFormat *)defaultPixelFormat {
|
||||
NSOpenGLPixelFormatAttribute attributes[] = {
|
||||
|
|
@ -172,6 +192,10 @@ static void renderUpdateCallback(void *callbackContext) {
|
|||
}
|
||||
self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
self.wantsBestResolutionOpenGLSurface = YES;
|
||||
_renderRequested.store(false);
|
||||
_drawableReady.store(false);
|
||||
_backingWidth.store(0);
|
||||
_backingHeight.store(0);
|
||||
return self;
|
||||
}
|
||||
|
||||
|
|
@ -184,56 +208,184 @@ static void renderUpdateCallback(void *callbackContext) {
|
|||
[[self openGLContext] makeCurrentContext];
|
||||
GLint swapInterval = 1;
|
||||
[[self openGLContext] setValues:&swapInterval forParameter:NSOpenGLContextParameterSwapInterval];
|
||||
_cglContext = [[self openGLContext] CGLContextObj];
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
[self updateBackingSize];
|
||||
[self startDisplayLinkIfNeeded];
|
||||
}
|
||||
|
||||
- (void)reshape {
|
||||
[super reshape];
|
||||
[[self openGLContext] update];
|
||||
CGLContextObj context = _cglContext ?: [[self openGLContext] CGLContextObj];
|
||||
if (context) {
|
||||
CGLLockContext(context);
|
||||
[[self openGLContext] update];
|
||||
CGLUnlockContext(context);
|
||||
} else {
|
||||
[[self openGLContext] update];
|
||||
}
|
||||
[self updateBackingSize];
|
||||
[self requestRender];
|
||||
}
|
||||
|
||||
- (void)requestRender {
|
||||
if (self.window) {
|
||||
self.needsDisplay = YES;
|
||||
- (void)viewDidMoveToWindow {
|
||||
[super viewDidMoveToWindow];
|
||||
[self updateBackingSize];
|
||||
[self requestRender];
|
||||
}
|
||||
|
||||
- (void)setRenderContext:(mpv_render_context *)renderContext {
|
||||
@synchronized (self) {
|
||||
_renderContext = renderContext;
|
||||
}
|
||||
[self requestRender];
|
||||
}
|
||||
|
||||
- (mpv_render_context *)renderContext {
|
||||
@synchronized (self) {
|
||||
return _renderContext;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)requestRender {
|
||||
_renderRequested.store(true);
|
||||
}
|
||||
|
||||
- (void)startDisplayLinkIfNeeded {
|
||||
if (_displayLink || !_cglContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
CVDisplayLinkRef displayLink = NULL;
|
||||
CVReturn createResult = CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
|
||||
if (createResult != kCVReturnSuccess || !displayLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
CVDisplayLinkSetOutputCallback(displayLink, displayLinkCallback, (__bridge void *)self);
|
||||
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(
|
||||
displayLink,
|
||||
_cglContext,
|
||||
[[self pixelFormat] CGLPixelFormatObj]
|
||||
);
|
||||
CVReturn startResult = CVDisplayLinkStart(displayLink);
|
||||
if (startResult != kCVReturnSuccess) {
|
||||
CVDisplayLinkRelease(displayLink);
|
||||
return;
|
||||
}
|
||||
_displayLink = displayLink;
|
||||
}
|
||||
|
||||
- (void)stopRendering {
|
||||
CVDisplayLinkRef displayLink = _displayLink;
|
||||
_displayLink = NULL;
|
||||
if (displayLink) {
|
||||
CVDisplayLinkStop(displayLink);
|
||||
CVDisplayLinkRelease(displayLink);
|
||||
}
|
||||
[self clearRenderContext];
|
||||
}
|
||||
|
||||
- (void)clearRenderContext {
|
||||
@synchronized (self) {
|
||||
_renderContext = nullptr;
|
||||
}
|
||||
_renderRequested.store(false);
|
||||
}
|
||||
|
||||
- (void)updateBackingSize {
|
||||
NSRect backingBounds = [self convertRectToBacking:self.bounds];
|
||||
int width = (int)llround(backingBounds.size.width);
|
||||
int height = (int)llround(backingBounds.size.height);
|
||||
_backingWidth.store(MAX(width, 0));
|
||||
_backingHeight.store(MAX(height, 0));
|
||||
_drawableReady.store(self.window != nil && width > 0 && height > 0);
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
(void)dirtyRect;
|
||||
[[self openGLContext] makeCurrentContext];
|
||||
|
||||
[self requestRender];
|
||||
if (!self.renderContext) {
|
||||
CGLContextObj context = _cglContext ?: [[self openGLContext] CGLContextObj];
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
CGLLockContext(context);
|
||||
CGLSetCurrentContext(context);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
[[self openGLContext] flushBuffer];
|
||||
CGLFlushDrawable(context);
|
||||
CGLUnlockContext(context);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)renderFrameFromDisplayLink {
|
||||
if (!_renderRequested.exchange(false)) {
|
||||
return;
|
||||
}
|
||||
if (!_drawableReady.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSRect backingBounds = [self convertRectToBacking:self.bounds];
|
||||
GLint currentFbo = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, ¤tFbo);
|
||||
@synchronized (self) {
|
||||
mpv_render_context *context = _renderContext;
|
||||
CGLContextObj glContext = _cglContext;
|
||||
int width = _backingWidth.load();
|
||||
int height = _backingHeight.load();
|
||||
if (!context || !glContext || width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
mpv_opengl_fbo fbo = {
|
||||
(int)currentFbo,
|
||||
(int)backingBounds.size.width,
|
||||
(int)backingBounds.size.height,
|
||||
GL_RGBA8
|
||||
};
|
||||
int flipY = 1;
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_OPENGL_FBO, &fbo},
|
||||
{MPV_RENDER_PARAM_FLIP_Y, &flipY},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
};
|
||||
CGLLockContext(glContext);
|
||||
CGLSetCurrentContext(glContext);
|
||||
|
||||
mpv_render_context_update(self.renderContext);
|
||||
mpv_render_context_render(self.renderContext, params);
|
||||
[[self openGLContext] flushBuffer];
|
||||
mpv_render_context_report_swap(self.renderContext);
|
||||
GLint currentFbo = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, ¤tFbo);
|
||||
|
||||
mpv_opengl_fbo fbo = {
|
||||
(int)currentFbo,
|
||||
width,
|
||||
height,
|
||||
GL_RGBA8
|
||||
};
|
||||
int flipY = 1;
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_OPENGL_FBO, &fbo},
|
||||
{MPV_RENDER_PARAM_FLIP_Y, &flipY},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
};
|
||||
|
||||
// MPV frame rendering is intentionally display-linked and off AppKit's
|
||||
// main thread so WKWebView controls stay responsive during playback.
|
||||
mpv_render_context_update(context);
|
||||
mpv_render_context_render(context, params);
|
||||
CGLFlushDrawable(glContext);
|
||||
mpv_render_context_report_swap(context);
|
||||
|
||||
CGLUnlockContext(glContext);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stopRendering];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static CVReturn displayLinkCallback(
|
||||
CVDisplayLinkRef /* displayLink */,
|
||||
const CVTimeStamp * /* now */,
|
||||
const CVTimeStamp * /* outputTime */,
|
||||
CVOptionFlags /* flagsIn */,
|
||||
CVOptionFlags * /* flagsOut */,
|
||||
void *displayLinkContext
|
||||
) {
|
||||
@autoreleasepool {
|
||||
PlayerOpenGLView *view = (__bridge PlayerOpenGLView *)displayLinkContext;
|
||||
[view renderFrameFromDisplayLink];
|
||||
}
|
||||
return kCVReturnSuccess;
|
||||
}
|
||||
|
||||
@implementation PlayerScriptHandler
|
||||
- (void)userContentController:(WKUserContentController *)userContentController
|
||||
didReceiveScriptMessage:(WKScriptMessage *)message {
|
||||
|
|
@ -462,7 +614,7 @@ static NSString *javaScriptStringLiteral(NSString *value) {
|
|||
_timer = nil;
|
||||
if (_renderContext) {
|
||||
mpv_render_context_set_update_callback(_renderContext, nullptr, nullptr);
|
||||
_videoView.renderContext = nullptr;
|
||||
[_videoView stopRendering];
|
||||
mpv_render_context_free(_renderContext);
|
||||
_renderContext = nullptr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2130,7 +2130,18 @@
|
|||
if (activeModal === "p2pConsent") renderP2pConsentModal();
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
const trackListSignature = tracks =>
|
||||
normalizeTracks(tracks)
|
||||
.map(track => [
|
||||
track.id == null ? "" : String(track.id),
|
||||
track.index == null ? "" : String(track.index),
|
||||
track.label == null ? "" : String(track.label),
|
||||
track.language == null ? "" : String(track.language),
|
||||
Boolean(track.selected) ? "1" : "0",
|
||||
].join(":"))
|
||||
.join("|");
|
||||
|
||||
const renderChrome = () => {
|
||||
const durationMs = Math.max(0, Number(state.durationMs) || 0);
|
||||
const positionMs = isScrubbing ? scrubPositionMs : Math.max(0, Number(state.positionMs) || 0);
|
||||
root.classList.toggle("locked", Boolean(state.isLocked));
|
||||
|
|
@ -2167,6 +2178,10 @@
|
|||
videoSettingsButton.setAttribute("aria-label", state.videoSettingsLabel || "Video settings");
|
||||
seek.disabled = Boolean(state.isLocked);
|
||||
setProgress(positionMs, durationMs);
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
renderChrome();
|
||||
renderActiveModal();
|
||||
};
|
||||
|
||||
|
|
@ -2419,16 +2434,26 @@
|
|||
render();
|
||||
});
|
||||
|
||||
window.playerUpdate = state => {
|
||||
const durationMs = Math.round((Number(state.duration) || 0) * 1000);
|
||||
const positionMs = Math.round((Number(state.position) || 0) * 1000);
|
||||
window.playerControls({
|
||||
window.playerUpdate = update => {
|
||||
const durationMs = Math.round((Number(update.duration) || 0) * 1000);
|
||||
const positionMs = Math.round((Number(update.position) || 0) * 1000);
|
||||
const audioTracks = normalizeTracks(update.audioTracks);
|
||||
const subtitleTracks = normalizeTracks(update.subtitleTracks);
|
||||
const audioTracksChanged = trackListSignature(audioTracks) !== trackListSignature(state.audioTracks);
|
||||
const subtitleTracksChanged = trackListSignature(subtitleTracks) !== trackListSignature(state.subtitleTracks);
|
||||
state = {
|
||||
...state,
|
||||
durationMs,
|
||||
positionMs,
|
||||
isPlaying: !Boolean(state.paused),
|
||||
audioTracks: normalizeTracks(state.audioTracks),
|
||||
subtitleTracks: normalizeTracks(state.subtitleTracks),
|
||||
});
|
||||
isPlaying: !Boolean(update.paused),
|
||||
audioTracks,
|
||||
subtitleTracks,
|
||||
};
|
||||
renderChrome();
|
||||
if ((audioTracksChanged && activeModal === "audio") ||
|
||||
(subtitleTracksChanged && activeModal === "subtitles")) {
|
||||
renderActiveModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.playerControls = nextState => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue