Compare commits

..

1 Commits

Author SHA1 Message Date
Toni Förster
e62010d5d5 improvements to MPVGLView
- add Pixel Buffer Object to (PBO)
- add some debug logging
- add scissor testing
- add dirty region checking

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-14 15:04:17 +02:00
6 changed files with 69 additions and 58 deletions

View File

@@ -4,7 +4,7 @@
<dict> <dict>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.78Z5H3M6RJ.stream.yattee.app.urlbookmarks</string> <string>group.ZYMM2HKXY2.yattee.app.url</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@@ -11,6 +11,7 @@ final class MPVOGLView: GLKView {
var mpvGL: UnsafeMutableRawPointer? var mpvGL: UnsafeMutableRawPointer?
var queue = DispatchQueue(label: "stream.yattee.opengl", qos: .userInteractive) var queue = DispatchQueue(label: "stream.yattee.opengl", qos: .userInteractive)
var needsDrawing = true var needsDrawing = true
private var dirtyRegion: CGRect?
override init(frame: CGRect) { override init(frame: CGRect) {
guard let context = EAGLContext(api: .openGLES2) else { guard let context = EAGLContext(api: .openGLES2) else {
@@ -85,6 +86,7 @@ final class MPVOGLView: GLKView {
@objc private func updateFrame() { @objc private func updateFrame() {
// Trigger the drawing process if needed // Trigger the drawing process if needed
if needsDrawing { if needsDrawing {
markRegionAsDirty(bounds)
setNeedsDisplay() setNeedsDisplay()
} }
} }
@@ -100,16 +102,60 @@ final class MPVOGLView: GLKView {
glClear(UInt32(GL_COLOR_BUFFER_BIT)) glClear(UInt32(GL_COLOR_BUFFER_BIT))
} }
// Function to set a dirty region when a part of the screen changes
func markRegionAsDirty(_ region: CGRect) {
if dirtyRegion == nil {
dirtyRegion = region
} else {
// Expand the dirty region to include the new region
dirtyRegion = dirtyRegion!.union(region)
}
}
// Logic to decide if only part of the screen needs updating
private func needsPartialUpdate() -> Bool {
// Check if there is a defined dirty region that needs updating
if let dirtyRegion, !dirtyRegion.isEmpty {
// Set up glScissor based on dirtyRegion coordinates
glScissor(GLint(dirtyRegion.origin.x), GLint(dirtyRegion.origin.y), GLsizei(dirtyRegion.width), GLsizei(dirtyRegion.height))
return true
}
return false
}
// Call this function when you know the entire screen needs updating
private func clearDirtyRegion() {
dirtyRegion = nil
}
override func draw(_: CGRect) { override func draw(_: CGRect) {
guard needsDrawing, let mpvGL else { return } guard needsDrawing, let mpvGL else { return }
// Ensure the correct context is set
guard EAGLContext.setCurrent(context) else {
logger.error("Failed to set current OpenGL context.")
return
}
// Bind the default framebuffer // Bind the default framebuffer
glGetIntegerv(UInt32(GL_FRAMEBUFFER_BINDING), &defaultFBO!) glGetIntegerv(UInt32(GL_FRAMEBUFFER_BINDING), &defaultFBO!)
// Ensure the framebuffer is valid
guard defaultFBO != nil && defaultFBO! != 0 else {
logger.error("Invalid framebuffer ID.")
return
}
// Get the current viewport dimensions // Get the current viewport dimensions
var dims: [GLint] = [0, 0, 0, 0] var dims: [GLint] = [0, 0, 0, 0]
glGetIntegerv(GLenum(GL_VIEWPORT), &dims) glGetIntegerv(GLenum(GL_VIEWPORT), &dims)
// Check if we need partial updates
if needsPartialUpdate() {
logger.info("Performing partial update with scissor test.")
glEnable(GLenum(GL_SCISSOR_TEST))
}
// Set up the OpenGL FBO data // Set up the OpenGL FBO data
var data = mpv_opengl_fbo( var data = mpv_opengl_fbo(
fbo: Int32(defaultFBO!), fbo: Int32(defaultFBO!),
@@ -129,10 +175,24 @@ final class MPVOGLView: GLKView {
mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: flipPtr), mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: flipPtr),
mpv_render_param() mpv_render_param()
] ]
mpv_render_context_render(OpaquePointer(mpvGL), &params) // Call the render function and check for errors
let result = mpv_render_context_render(OpaquePointer(mpvGL), &params)
if result < 0 {
logger.error("mpv_render_context_render() failed with error code: \(result)")
} else {
logger.info("mpv_render_context_render() called successfully.")
} }
} }
} }
// Disable the scissor test after rendering if it was enabled
if needsPartialUpdate() {
glDisable(GLenum(GL_SCISSOR_TEST))
}
// Clear dirty region after drawing
clearDirtyRegion()
}
} }
extension Notification.Name { extension Notification.Name {

View File

@@ -43,7 +43,7 @@ struct PlayerBackendView: View {
Color.clear Color.clear
.onAppear { player.playerSize = proxy.size } .onAppear { player.playerSize = proxy.size }
.onChange(of: proxy.size) { _ in player.playerSize = proxy.size } .onChange(of: proxy.size) { _ in player.playerSize = proxy.size }
.onChange(of: player.currentItem?.id) { _ in player.playerSize = proxy.size } .onChange(of: player.controls.presentingOverlays) { _ in player.playerSize = proxy.size }
}) })
#if !os(tvOS) #if !os(tvOS)

View File

@@ -56,6 +56,7 @@ extension VideoPlayerView {
player.seek.gestureStart = time player.seek.gestureStart = time
} }
let timeSeek = (time / player.playerSize.width) * horizontalDrag * seekGestureSpeed let timeSeek = (time / player.playerSize.width) * horizontalDrag * seekGestureSpeed
player.seek.gestureSeek = timeSeek player.seek.gestureSeek = timeSeek
} }
return return
@@ -79,54 +80,6 @@ extension VideoPlayerView {
} }
} }
var detailsDragGesture: some Gesture {
DragGesture(minimumDistance: 30)
.onChanged { value in
handleDetailsDragChange(value)
}
.onEnded { value in
handleDetailsDragEnd(value)
}
}
private func handleDetailsDragChange(_ value: DragGesture.Value) {
let maxOffset = -player.playerSize.height
// Continuous drag update for smooth movement of VideoDetails
if fullScreenDetails {
// Allow only downward dragging when in fullscreen
if value.translation.height > 0 {
detailViewDragOffset = min(value.translation.height, abs(maxOffset))
}
} else {
// Allow only upward dragging when not in fullscreen
if value.translation.height < 0 {
detailViewDragOffset = max(value.translation.height, maxOffset)
}
}
}
private func handleDetailsDragEnd(_ value: DragGesture.Value) {
if value.translation.height < -50, !fullScreenDetails {
// Swipe up to enter fullscreen
withAnimation(Constants.overlayAnimation) {
fullScreenDetails = true
detailViewDragOffset = 0
}
} else if value.translation.height > 50, fullScreenDetails {
// Swipe down to exit fullscreen
withAnimation(Constants.overlayAnimation) {
fullScreenDetails = false
detailViewDragOffset = 0
}
} else {
// Reset offset if drag was not significant
withAnimation(Constants.overlayAnimation) {
detailViewDragOffset = 0
}
}
}
func onPlayerDragGestureEnded() { func onPlayerDragGestureEnded() {
if horizontalPlayerGestureEnabled, isHorizontalDrag { if horizontalPlayerGestureEnabled, isHorizontalDrag {
isHorizontalDrag = false isHorizontalDrag = false
@@ -155,6 +108,7 @@ extension VideoPlayerView {
} }
} }
// Function to temporarily disable the toggle gesture after a fullscreen change
private func disableGestureTemporarily() { private func disableGestureTemporarily() {
disableToggleGesture = true disableToggleGesture = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {

View File

@@ -223,7 +223,7 @@ struct VideoDetails: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle()) .contentShape(Rectangle())
.padding(.horizontal, 16) .padding(.horizontal, 16)
// swiftlint:disable trailing_closure
// TODO: when setting tvOS minimum to 16, the platform modifier can be removed // TODO: when setting tvOS minimum to 16, the platform modifier can be removed
#if !os(tvOS) #if !os(tvOS)
.simultaneousGesture( // Simultaneous gesture to prioritize button tap .simultaneousGesture( // Simultaneous gesture to prioritize button tap
@@ -234,7 +234,7 @@ struct VideoDetails: View {
} }
) )
#endif #endif
// swiftlint:enable trailing_closure
if VideoActions().isAnyActionVisible() { if VideoActions().isAnyActionVisible() {
VideoActions(video: player.videoForDisplay) VideoActions(video: player.videoForDisplay)
.padding(.vertical, 5) .padding(.vertical, 5)

View File

@@ -24,12 +24,13 @@ struct VideoPlayerView: View {
#if os(macOS) #if os(macOS)
335 335
#else #else
140 200
#endif #endif
} }
@State private var playerSize: CGSize = .zero { didSet { updateSidebarQueue() } } @State private var playerSize: CGSize = .zero { didSet { updateSidebarQueue() } }
@State private var hoveringPlayer = false @State private var hoveringPlayer = false
@State private var fullScreenDetails = false
@State private var sidebarQueue = defaultSidebarQueueValue @State private var sidebarQueue = defaultSidebarQueueValue
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@@ -50,14 +51,12 @@ struct VideoPlayerView: View {
@State var isHorizontalDrag = false @State var isHorizontalDrag = false
@State var isVerticalDrag = false @State var isVerticalDrag = false
@State var viewDragOffset = Self.hiddenOffset @State var viewDragOffset = Self.hiddenOffset
@State var detailViewDragOffset: Double = 0
// swiftlint:enable private_swiftui_state // swiftlint:enable private_swiftui_state
#endif #endif
// swiftlint:disable private_swiftui_state // swiftlint:disable private_swiftui_state
@State var disableToggleGesture = false @State var disableToggleGesture = false
@State var fullScreenDetails = false
// swiftlint:enable private_swiftui_state // swiftlint:enable private_swiftui_state
@ObservedObject var player = PlayerModel.shared // swiftlint:disable:this swiftui_state_private @ObservedObject var player = PlayerModel.shared // swiftlint:disable:this swiftui_state_private
@@ -308,8 +307,6 @@ struct VideoPlayerView: View {
#endif #endif
.id(player.currentVideo?.cacheKey) .id(player.currentVideo?.cacheKey)
.transition(.opacity) .transition(.opacity)
.offset(y: detailViewDragOffset)
.gesture(detailsDragGesture)
} else { } else {
VStack {} VStack {}
} }