DamusVideoPlayer.swift (6467B)
1 // 2 // VideoPlayerView.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-04-05. 6 // 7 8 import SwiftUI 9 10 /// get coordinates in Global reference frame given a Local point & geometry 11 func globalCoordinate(localX x: CGFloat, localY y: CGFloat, 12 localGeometry geo: GeometryProxy) -> CGPoint { 13 let localPoint = CGPoint(x: x, y: y) 14 return geo.frame(in: .global).origin.applying( 15 .init(translationX: localPoint.x, y: localPoint.y) 16 ) 17 } 18 19 struct DamusVideoPlayer: View { 20 let url: URL 21 @StateObject var model: DamusVideoPlayerViewModel 22 @EnvironmentObject private var orientationTracker: OrientationTracker 23 let style: Style 24 let visibility_tracking_method: VisibilityTrackingMethod 25 @State var isVisible: Bool = false 26 27 init(url: URL, video_size: Binding<CGSize?>, controller: VideoController, style: Style, visibility_tracking_method: VisibilityTrackingMethod = .y_scroll) { 28 self.url = url 29 let mute: Bool? 30 if case .full = style { 31 mute = false 32 } 33 else { 34 mute = nil 35 } 36 _model = StateObject(wrappedValue: DamusVideoPlayerViewModel.cached_video_model(url: url, video_size: video_size, controller: controller, mute: mute)) 37 self.visibility_tracking_method = visibility_tracking_method 38 self.style = style 39 } 40 41 var body: some View { 42 GeometryReader { geo in 43 let localFrame = geo.frame(in: .local) 44 let centerY = globalCoordinate(localX: 0, localY: localFrame.midY, localGeometry: geo).y 45 ZStack { 46 if case .full = self.style { 47 DamusAVPlayerView(player: model.player, controller: model.player_view_controller, show_playback_controls: true) 48 } 49 if case .preview(let on_tap) = self.style { 50 DamusAVPlayerView(player: model.player, controller: model.player_view_controller, show_playback_controls: false) 51 .simultaneousGesture(TapGesture().onEnded({ 52 on_tap?() 53 })) 54 } 55 56 if model.is_loading { 57 ProgressView() 58 .progressViewStyle(.circular) 59 .tint(.white) 60 .scaleEffect(CGSize(width: 1.5, height: 1.5)) 61 } 62 63 if case .preview = self.style { 64 if model.has_audio { 65 mute_button 66 } 67 } 68 if model.is_live { 69 live_indicator 70 } 71 } 72 .onChange(of: centerY) { _ in 73 if case .y_scroll = visibility_tracking_method { 74 update_is_visible(centerY: centerY) 75 } 76 } 77 .on_visibility_change(perform: { new_visibility in 78 if case .generic = visibility_tracking_method { 79 model.set_view_is_visible(new_visibility) 80 } 81 }) 82 .onAppear { 83 if case .y_scroll = visibility_tracking_method { 84 update_is_visible(centerY: centerY) 85 } 86 } 87 } 88 .onDisappear { 89 if case .y_scroll = visibility_tracking_method { 90 model.view_did_disappear() 91 } 92 } 93 } 94 95 private func update_is_visible(centerY: CGFloat) { 96 let isBelowTop = centerY > 100, /// 100 =~ approx. bottom (y) of ContentView's TabView 97 isAboveBottom = centerY < orientationTracker.deviceMajorAxis 98 model.set_view_is_visible(isBelowTop && isAboveBottom) 99 } 100 101 private var mute_icon: String { 102 !model.has_audio || model.is_muted ? "speaker.slash" : "speaker" 103 } 104 105 private var mute_icon_color: Color { 106 model.has_audio ? .white : .red 107 } 108 109 private var mute_button: some View { 110 HStack { 111 Spacer() 112 VStack { 113 Spacer() 114 115 Button { 116 model.did_tap_mute_button() 117 } label: { 118 ZStack { 119 Circle() 120 .opacity(0.2) 121 .frame(width: 32, height: 32) 122 .foregroundColor(.black) 123 124 Image(systemName: mute_icon) 125 .padding() 126 .foregroundColor(mute_icon_color) 127 } 128 } 129 } 130 } 131 } 132 133 private var live_indicator: some View { 134 VStack { 135 HStack { 136 Text("LIVE", comment: "Text indicator that the video is a livestream.") 137 .bold() 138 .foregroundColor(.red) 139 .padding(.horizontal) 140 .padding(.vertical, 5) 141 .background( 142 Capsule() 143 .fill(Color.black.opacity(0.5)) 144 ) 145 .padding([.top, .leading]) 146 Spacer() 147 } 148 Spacer() 149 } 150 } 151 152 enum Style { 153 /// A full video player with playback controls 154 case full 155 /// A style suitable for muted, auto-playing videos on a feed 156 case preview(on_tap: (() -> Void)?) 157 } 158 159 enum VisibilityTrackingMethod { 160 /// Detects visibility based on its Y position relative to viewport. Ideal for long feeds 161 case y_scroll 162 /// Detects visibility based whether the view intersects with the viewport 163 case generic 164 } 165 } 166 struct DamusVideoPlayer_Previews: PreviewProvider { 167 static var previews: some View { 168 Group { 169 DamusVideoPlayer(url: URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!, video_size: .constant(nil), controller: VideoController(), style: .full) 170 .environmentObject(OrientationTracker()) 171 .previewDisplayName("Full video player") 172 173 DamusVideoPlayer(url: URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!, video_size: .constant(nil), controller: VideoController(), style: .preview(on_tap: nil)) 174 .environmentObject(OrientationTracker()) 175 .previewDisplayName("Preview video player") 176 } 177 } 178 }