damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }