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:
Daniel D’Aquino
2024-03-18 22:21:21 +00:00
committed by William Casarin
parent 98eddf1337
commit 671b0b67ce
9 changed files with 225 additions and 133 deletions

View File

@@ -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) {

View File

@@ -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")
}
}
}

View File

@@ -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(