1
0
mirror of git://jb55.com/damus synced 2024-09-28 16:00:43 +00:00

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

@ -377,7 +377,7 @@
4CFD502F2A2DA45800A229DB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFD502E2A2DA45800A229DB /* MediaView.swift */; };
4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */; };
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; };
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; };
4CFF8F6729CC9E3A008DB934 /* FullScreenCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* FullScreenCarouselView.swift */; };
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */; };
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */; };
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; };
@ -388,7 +388,7 @@
504323A72A34915F006AE6DC /* RelayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504323A62A34915F006AE6DC /* RelayModel.swift */; };
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504323A82A3495B6006AE6DC /* RelayModelCache.swift */; };
5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */; };
50A16FFB2AA6C06600DFEC1F /* AVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFA2AA6C06600DFEC1F /* AVPlayerView.swift */; };
50A16FFB2AA6C06600DFEC1F /* DamusAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFA2AA6C06600DFEC1F /* DamusAVPlayerView.swift */; };
50A16FFD2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFC2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift */; };
50A16FFF2AA76A0900DFEC1F /* VideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFE2AA76A0900DFEC1F /* VideoController.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
@ -433,8 +433,6 @@
B59CAD4D2B688D1000677E8B /* MutelistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533694D2B66D791008A805E /* MutelistManager.swift */; };
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */; };
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; };
BA0F0A6F2B36207E001641B2 /* CameraMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */; };
BA10192F2B449556009C57DA /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA10192E2B449556009C57DA /* CameraPreview.swift */; };
B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; };
B5C60C212B530D5600C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; };
B5C60C232B532A8700C5ECA7 /* DamusDuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */; };
@ -1300,7 +1298,7 @@
4CFD502E2A2DA45800A229DB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleView.swift; sourceTree = "<group>"; };
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = "<group>"; };
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
4CFF8F6629CC9E3A008DB934 /* FullScreenCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCarouselView.swift; sourceTree = "<group>"; };
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContainerView.swift; sourceTree = "<group>"; };
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedEvent.swift; sourceTree = "<group>"; };
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; };
@ -1311,7 +1309,7 @@
504323A62A34915F006AE6DC /* RelayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayModel.swift; sourceTree = "<group>"; };
504323A82A3495B6006AE6DC /* RelayModelCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayModelCache.swift; sourceTree = "<group>"; };
5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = "<group>"; };
50A16FFA2AA6C06600DFEC1F /* AVPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerView.swift; sourceTree = "<group>"; };
50A16FFA2AA6C06600DFEC1F /* DamusAVPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAVPlayerView.swift; sourceTree = "<group>"; };
50A16FFC2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusVideoPlayerViewModel.swift; sourceTree = "<group>"; };
50A16FFE2AA76A0900DFEC1F /* VideoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoController.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
@ -1355,8 +1353,6 @@
B57B4C652B312C3700A232C0 /* NostrAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NostrAuth.swift; sourceTree = "<group>"; };
B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItemTests.swift; sourceTree = "<group>"; usesTabs = 0; };
B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = "<group>"; usesTabs = 0; };
BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraMediaView.swift; sourceTree = "<group>"; };
BA10192E2B449556009C57DA /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItem.swift; sourceTree = "<group>"; usesTabs = 0; };
B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusDuration.swift; sourceTree = "<group>"; usesTabs = 0; };
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
@ -1733,7 +1729,7 @@
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */,
50A16FFC2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift */,
50A16FFE2AA76A0900DFEC1F /* VideoController.swift */,
50A16FFA2AA6C06600DFEC1F /* AVPlayerView.swift */,
50A16FFA2AA6C06600DFEC1F /* DamusAVPlayerView.swift */,
);
path = Video;
sourceTree = "<group>";
@ -2659,7 +2655,7 @@
isa = PBXGroup;
children = (
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */,
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */,
4CFF8F6629CC9E3A008DB934 /* FullScreenCarouselView.swift */,
6439E013296790CF0020672B /* ProfilePicImageView.swift */,
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */,
4CFD502E2A2DA45800A229DB /* MediaView.swift */,
@ -2744,14 +2740,6 @@
path = DamusNotificationService;
sourceTree = "<group>";
};
E06336A72B7582D600A88E6B /* Assets */ = {
isa = PBXGroup;
children = (
E06336A82B7582E000A88E6B /* img_with_location.jpeg */,
);
path = Assets;
sourceTree = "<group>";
};
D7CBD1D22B8D21C100BFD889 /* Extensions */ = {
isa = PBXGroup;
children = (
@ -2760,6 +2748,14 @@
path = Extensions;
sourceTree = "<group>";
};
E06336A72B7582D600A88E6B /* Assets */ = {
isa = PBXGroup;
children = (
E06336A82B7582E000A88E6B /* img_with_location.jpeg */,
);
path = Assets;
sourceTree = "<group>";
};
F71694E82A66221E001F4053 /* Onboarding */ = {
isa = PBXGroup;
children = (
@ -3178,7 +3174,7 @@
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
D7ADD3E02B538D4200F104C4 /* DamusPurpleURLSheetView.swift in Sources */,
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */,
4CFF8F6729CC9E3A008DB934 /* FullScreenCarouselView.swift in Sources */,
4CA927632A290EB10098A105 /* EventTop.swift in Sources */,
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */,
4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */,
@ -3416,7 +3412,7 @@
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */,
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
50A16FFB2AA6C06600DFEC1F /* AVPlayerView.swift in Sources */,
50A16FFB2AA6C06600DFEC1F /* DamusAVPlayerView.swift in Sources */,
4CA352A82A76B37E003BB08B /* NewMutesNotify.swift in Sources */,
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
7527271E2A93FF0100214108 /* Block.swift in Sources */,

View File

@ -132,7 +132,7 @@ struct ImageCarousel: View {
model.open_sheet = true
}
case .video(let url):
DamusVideoPlayer(url: url, video_size: $model.video_size, controller: state.video)
DamusVideoPlayer(url: url, video_size: $model.video_size, controller: state.video, style: .preview(on_tap: { model.open_sheet = true }))
.onChange(of: model.video_size) { size in
guard let size else { return }
@ -201,7 +201,7 @@ struct ImageCarousel: View {
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.fullScreenCover(isPresented: $model.open_sheet) {
ImageView(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex)
FullScreenCarouselView(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex)
}
.frame(height: height)
.onChange(of: model.selectedIndex) { value in
@ -296,7 +296,9 @@ public struct ImageFill {
struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View {
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
ImageCarousel(state: test_damus_state, evid: test_note.id, urls: [url, url])
let test_video_url: MediaUrl = .video(URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!)
ImageCarousel(state: test_damus_state, evid: test_note.id, urls: [test_video_url, url])
.environmentObject(OrientationTracker())
}
}

View File

@ -0,0 +1,127 @@
//
// FullScreenCarouselView.swift
// damus
//
// Created by William Casarin on 2023-03-23.
//
import SwiftUI
struct FullScreenCarouselView: View {
let video_controller: VideoController
let urls: [MediaUrl]
@Environment(\.presentationMode) var presentationMode
@State var showMenu = true
let settings: UserSettingsStore
@Binding var selectedIndex: Int
var tabViewIndicator: some View {
HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in
Capsule()
.fill(index == selectedIndex ? Color.white : Color.damusMediumGrey)
.frame(width: 7, height: 7)
.onTapGesture {
selectedIndex = index
}
}
}
.padding()
.clipShape(Capsule())
}
var background: some ShapeStyle {
if case .video = urls[safe: selectedIndex] {
return AnyShapeStyle(Color.black)
}
else {
return AnyShapeStyle(.regularMaterial)
}
}
var background_color: UIColor {
return .black
}
var body: some View {
ZStack {
Color(self.background_color)
.ignoresSafeArea()
TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in
VStack {
if case .video = urls[safe: index] {
ImageContainerView(video_controller: video_controller, url: urls[index], settings: settings)
.clipped() // SwiftUI hack from https://stackoverflow.com/a/74401288 to make playback controls show up within the TabView
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
.ignoresSafeArea()
}
else {
ZoomableScrollView {
ImageContainerView(video_controller: video_controller, url: urls[index], settings: settings)
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
}
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
.ignoresSafeArea()
}
}.tag(index)
}
}
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.gesture(TapGesture(count: 2).onEnded {
// Prevents menu from hiding on double tap
})
.gesture(TapGesture(count: 1).onEnded {
showMenu.toggle()
})
.overlay(
GeometryReader { geo in
VStack {
if showMenu {
NavDismissBarView(showBackgroundCircle: false)
.foregroundColor(.white)
Spacer()
if (urls.count > 1) {
tabViewIndicator
}
}
}
.animation(.easeInOut, value: showMenu)
.padding(.bottom, geo.safeAreaInsets.bottom == 0 ? 12 : 0)
}
)
}
}
}
fileprivate struct ImageViewPreview: View {
@State var selectedIndex: Int = 0
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
let test_video_url: MediaUrl = .video(URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!)
var body: some View {
FullScreenCarouselView(video_controller: test_damus_state.video, urls: [test_video_url, url], settings: test_damus_state.settings, selectedIndex: $selectedIndex)
.environmentObject(OrientationTracker())
}
}
struct ImageView_Previews: PreviewProvider {
static var previews: some View {
ImageViewPreview()
}
}

View File

@ -43,19 +43,26 @@ struct ImageContainerView: View {
var body: some View {
Group {
switch url {
case .image(let url):
Img(url: url)
case .video(let url):
DamusVideoPlayer(url: url, video_size: .constant(nil), controller: video_controller)
case .image(let url):
Img(url: url)
case .video(let url):
DamusVideoPlayer(url: url, video_size: .constant(nil), controller: video_controller, style: .full)
}
}
}
}
let test_image_url = URL(string: "https://jb55.com/red-me.jpg")!
fileprivate let test_video_url = URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!
struct ImageContainerView_Previews: PreviewProvider {
static var previews: some View {
ImageContainerView(video_controller: test_damus_state.video, url: .image(test_image_url), settings: test_damus_state.settings)
Group {
ImageContainerView(video_controller: test_damus_state.video, url: .image(test_image_url), settings: test_damus_state.settings)
.previewDisplayName("Image")
ImageContainerView(video_controller: test_damus_state.video, url: .video(test_video_url), settings: test_damus_state.settings)
.previewDisplayName("Video")
}
.environmentObject(OrientationTracker())
}
}

View File

@ -1,90 +0,0 @@
//
// ImageView.swift
// damus
//
// Created by William Casarin on 2023-03-23.
//
import SwiftUI
struct ImageView: View {
let video_controller: VideoController
let urls: [MediaUrl]
@Environment(\.presentationMode) var presentationMode
@State var showMenu = true
let settings: UserSettingsStore
@Binding var selectedIndex: Int
var tabViewIndicator: some View {
HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in
Capsule()
.fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
.frame(width: 7, height: 7)
.onTapGesture {
selectedIndex = index
}
}
}
.padding()
.background(.regularMaterial)
.clipShape(Capsule())
}
var body: some View {
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in
ZoomableScrollView {
ImageContainerView(video_controller: video_controller, url: urls[index], settings: settings)
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
}
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
.ignoresSafeArea()
.tag(index)
}
}
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.gesture(TapGesture(count: 2).onEnded {
// Prevents menu from hiding on double tap
})
.gesture(TapGesture(count: 1).onEnded {
showMenu.toggle()
})
.overlay(
GeometryReader { geo in
VStack {
if showMenu {
NavDismissBarView()
Spacer()
if (urls.count > 1) {
tabViewIndicator
}
}
}
.animation(.easeInOut, value: showMenu)
.padding(.bottom, geo.safeAreaInsets.bottom == 0 ? 12 : 0)
}
)
}
}
}
struct ImageView_Previews: PreviewProvider {
static var previews: some View {
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
ImageView(video_controller: test_damus_state.video, urls: [url], settings: test_damus_state.settings, selectedIndex: Binding.constant(0))
}
}

View File

@ -42,16 +42,27 @@ struct ProfileImageContainerView: View {
struct NavDismissBarView: View {
@Environment(\.presentationMode) var presentationMode
let showBackgroundCircle: Bool
init(showBackgroundCircle: Bool = true) {
self.showBackgroundCircle = showBackgroundCircle
}
var body: some View {
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image("close")
.frame(width: 33, height: 33)
.background(.regularMaterial)
.clipShape(Circle())
if showBackgroundCircle {
Image("close")
.frame(width: 33, height: 33)
.background(.regularMaterial)
.clipShape(Circle())
}
else {
Image("close")
.frame(width: 33, height: 33)
}
})
Spacer()

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(