Add playback controls to videos
This commit includes several UX changes to give users better control over video playback. It also, by design, work arounds a SwiftUI quirk* Here are the changes to the UX: 1. Videos on the feed only have a mute/unmute button 2. When the user clicks on the video, they are taken to a full screen carousel view (similar to when you click on an image) 3. The full-screen carousel view shows all video playback controls (through a specific SwiftUI hack) 4. If the carousel has multiple videos/images, the user can swipe between them normally as expected Other UI changes that were made: - The full screen carousel now uses dark mode (black background, white close button) * The SwiftUI quirk is that when video views are placed within a TabView with ".page" tab view style, the tabview consumes most of the user gestures, making the video playback controls unusable. Changelog-Changed: Improve UX around video playback Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Link: 20240318222048.14226-3-daniel@daquino.me Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
committed by
William Casarin
parent
98eddf1337
commit
671b0b67ce
@@ -9,12 +9,15 @@ import Foundation
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
|
||||
struct AVPlayerView: UIViewControllerRepresentable {
|
||||
struct DamusAVPlayerView: UIViewControllerRepresentable {
|
||||
|
||||
let player: AVPlayer
|
||||
var controller: AVPlayerViewController
|
||||
let show_playback_controls: Bool
|
||||
|
||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||
AVPlayerViewController()
|
||||
self.controller.showsPlaybackControls = show_playback_controls
|
||||
return self.controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
|
||||
@@ -20,10 +20,19 @@ struct DamusVideoPlayer: View {
|
||||
let url: URL
|
||||
@StateObject var model: DamusVideoPlayerViewModel
|
||||
@EnvironmentObject private var orientationTracker: OrientationTracker
|
||||
let style: Style
|
||||
|
||||
init(url: URL, video_size: Binding<CGSize?>, controller: VideoController) {
|
||||
init(url: URL, video_size: Binding<CGSize?>, controller: VideoController, style: Style) {
|
||||
self.url = url
|
||||
_model = StateObject(wrappedValue: DamusVideoPlayerViewModel(url: url, video_size: video_size, controller: controller))
|
||||
let mute: Bool?
|
||||
if case .full = style {
|
||||
mute = false
|
||||
}
|
||||
else {
|
||||
mute = nil
|
||||
}
|
||||
_model = StateObject(wrappedValue: DamusVideoPlayerViewModel(url: url, video_size: video_size, controller: controller, mute: mute))
|
||||
self.style = style
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -31,7 +40,15 @@ struct DamusVideoPlayer: View {
|
||||
let localFrame = geo.frame(in: .local)
|
||||
let centerY = globalCoordinate(localX: 0, localY: localFrame.midY, localGeometry: geo).y
|
||||
ZStack {
|
||||
AVPlayerView(player: model.player)
|
||||
if case .full = self.style {
|
||||
DamusAVPlayerView(player: model.player, controller: model.player_view_controller, show_playback_controls: true)
|
||||
}
|
||||
if case .preview(let on_tap) = self.style {
|
||||
DamusAVPlayerView(player: model.player, controller: model.player_view_controller, show_playback_controls: false)
|
||||
.simultaneousGesture(TapGesture().onEnded({
|
||||
on_tap?()
|
||||
}))
|
||||
}
|
||||
|
||||
if model.is_loading {
|
||||
ProgressView()
|
||||
@@ -40,8 +57,10 @@ struct DamusVideoPlayer: View {
|
||||
.scaleEffect(CGSize(width: 1.5, height: 1.5))
|
||||
}
|
||||
|
||||
if model.has_audio {
|
||||
mute_button
|
||||
if case .preview = self.style {
|
||||
if model.has_audio {
|
||||
mute_button
|
||||
}
|
||||
}
|
||||
if model.is_live {
|
||||
live_indicator
|
||||
@@ -115,9 +134,24 @@ struct DamusVideoPlayer: View {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
enum Style {
|
||||
/// A full video player with playback controls
|
||||
case full
|
||||
/// A style suitable for muted, auto-playing videos on a feed
|
||||
case preview(on_tap: (() -> Void)?)
|
||||
}
|
||||
}
|
||||
struct DamusVideoPlayer_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DamusVideoPlayer(url: URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!, video_size: .constant(nil), controller: VideoController())
|
||||
Group {
|
||||
DamusVideoPlayer(url: URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!, video_size: .constant(nil), controller: VideoController(), style: .full)
|
||||
.environmentObject(OrientationTracker())
|
||||
.previewDisplayName("Full video player")
|
||||
|
||||
DamusVideoPlayer(url: URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!, video_size: .constant(nil), controller: VideoController(), style: .preview(on_tap: nil))
|
||||
.environmentObject(OrientationTracker())
|
||||
.previewDisplayName("Preview video player")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
@@ -27,7 +28,8 @@ final class DamusVideoPlayerViewModel: ObservableObject {
|
||||
private let url: URL
|
||||
private let player_item: AVPlayerItem
|
||||
let player: AVPlayer
|
||||
private let controller: VideoController
|
||||
fileprivate let controller: VideoController
|
||||
let player_view_controller = AVPlayerViewController()
|
||||
let id = UUID()
|
||||
|
||||
@Published var has_audio = false
|
||||
@@ -55,7 +57,7 @@ final class DamusVideoPlayerViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
init(url: URL, video_size: Binding<CGSize?>, controller: VideoController) {
|
||||
init(url: URL, video_size: Binding<CGSize?>, controller: VideoController, mute: Bool? = nil) {
|
||||
self.url = url
|
||||
player_item = AVPlayerItem(url: url)
|
||||
player = AVPlayer(playerItem: player_item)
|
||||
@@ -66,7 +68,7 @@ final class DamusVideoPlayerViewModel: ObservableObject {
|
||||
await load()
|
||||
}
|
||||
|
||||
is_muted = controller.should_mute_video(url: url)
|
||||
is_muted = mute ?? controller.should_mute_video(url: url)
|
||||
player.isMuted = is_muted
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
|
||||
Reference in New Issue
Block a user