feat: 清空播放器

This commit is contained in:
jinyaqiu 2025-09-02 16:58:51 +08:00
parent c221f91412
commit 0670c686d2
2 changed files with 72 additions and 118 deletions

View File

@ -2,7 +2,6 @@ import SwiftUI
import AVKit import AVKit
import os.log import os.log
/// A view that displays either an image or a video with fullscreen support
struct BlindOutcomeView: View { struct BlindOutcomeView: View {
let media: MediaType let media: MediaType
let time: String? let time: String?
@ -12,6 +11,7 @@ struct BlindOutcomeView: View {
@State private var isPlaying = false @State private var isPlaying = false
@State private var showControls = true @State private var showControls = true
@State private var showIPListModal = false @State private var showIPListModal = false
@State private var player: AVPlayer?
init(media: MediaType, time: String? = nil, description: String? = nil) { init(media: MediaType, time: String? = nil, description: String? = nil) {
self.media = media self.media = media
@ -28,7 +28,6 @@ struct BlindOutcomeView: View {
// //
HStack { HStack {
Button(action: { Button(action: {
//
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
}) { }) {
HStack(spacing: 4) { HStack(spacing: 4) {
@ -47,7 +46,6 @@ struct BlindOutcomeView: View {
Spacer() Spacer()
//
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "chevron.left") Image(systemName: "chevron.left")
.opacity(0) .opacity(0)
@ -56,7 +54,7 @@ struct BlindOutcomeView: View {
} }
.padding(.vertical, 12) .padding(.vertical, 12)
.background(Color.themeTextWhiteSecondary) .background(Color.themeTextWhiteSecondary)
.zIndex(1) // .zIndex(1)
Spacer() Spacer()
.frame(height: 30) .frame(height: 30)
@ -65,7 +63,6 @@ struct BlindOutcomeView: View {
GeometryReader { geometry in GeometryReader { geometry in
VStack(spacing: 16) { VStack(spacing: 16) {
ZStack { ZStack {
//
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.fill(Color.white) .fill(Color.white)
.shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2) .shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2)
@ -86,48 +83,41 @@ struct BlindOutcomeView: View {
} }
case .video(let url, _): case .video(let url, _):
VideoPlayerView(url: url, isPlaying: $isPlaying) VideoPlayerView(url: url, isPlaying: $isPlaying, player: $player)
.frame(width: UIScreen.main.bounds.width - 40) .frame(width: UIScreen.main.bounds.width - 40)
.background(Color.clear) .background(Color.clear)
.cornerRadius(10) .cornerRadius(10)
.clipped() .clipped()
.onAppear { .onAppear {
// Auto-play the video when it appears
isPlaying = true isPlaying = true
} }
.onDisappear {
isPlaying = false
player?.pause()
}
.onTapGesture { .onTapGesture {
withAnimation { withAnimation {
showControls.toggle() showControls.toggle()
} }
} }
.fullScreenCover(isPresented: $isFullscreen) { .fullScreenCover(isPresented: $isFullscreen) {
FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: nil) FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: player)
} }
.overlay(
showControls ? VideoControls(
isPlaying: $isPlaying,
onClose: { isFullscreen = false }
) : nil
)
} }
VStack(alignment: .leading, spacing: 8) { if let description = description, !description.isEmpty {
VStack(alignment: .leading, spacing: 2) {
if let description = description, !description.isEmpty { Text("Description")
VStack(alignment: .leading, spacing: 2) { .font(Typography.font(for: .body, family: .quicksandBold))
Text("Description") .foregroundColor(.themeTextMessageMain)
.font(Typography.font(for: .body, family: .quicksandBold)) Text(description)
.foregroundColor(.themeTextMessageMain) .font(.system(size: 12))
Text(description) .foregroundColor(Color.themeTextMessageMain)
.font(.system(size: 12)) .fixedSize(horizontal: false, vertical: true)
.foregroundColor(Color.themeTextMessageMain)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 12)
.padding(.bottom, 12)
} }
.padding(.horizontal, 12)
.padding(.bottom, 12)
} }
.frame(maxWidth: .infinity, alignment: .leading)
} }
.padding(.top, 8) .padding(.top, 8)
} }
@ -136,12 +126,13 @@ struct BlindOutcomeView: View {
.padding(.bottom, 20) .padding(.bottom, 20)
} }
.padding(.horizontal) .padding(.horizontal)
Spacer() Spacer()
// Button at bottom // Button at bottom
VStack { VStack {
Spacer() Spacer()
Button(action: { Button(action: {
// video
if case .video = media { if case .video = media {
withAnimation { withAnimation {
showIPListModal = true showIPListModal = true
@ -162,22 +153,20 @@ struct BlindOutcomeView: View {
} }
.padding(.bottom, 20) .padding(.bottom, 20)
} }
.onDisappear {
// Clean up video player when view disappears
if case .video = media {
isPlaying = false
}
}
} }
.navigationBarHidden(true) // .navigationBarHidden(true)
.navigationBarBackButtonHidden(true) // .navigationBarBackButtonHidden(true)
.statusBar(hidden: isFullscreen) .statusBar(hidden: isFullscreen)
} }
.navigationViewStyle(StackNavigationViewStyle()) // iPad .navigationViewStyle(StackNavigationViewStyle())
.navigationBarHidden(true) // .navigationBarHidden(true)
.overlay( .overlay(
JoinModal(isPresented: $showIPListModal) JoinModal(isPresented: $showIPListModal)
) )
.onDisappear {
player?.pause()
player = nil
}
} }
} }
@ -187,22 +176,19 @@ private struct FullscreenMediaView: View {
@Binding var isPresented: Bool @Binding var isPresented: Bool
@Binding var isPlaying: Bool @Binding var isPlaying: Bool
@State private var showControls = true @State private var showControls = true
@State private var player: AVPlayer? private let player: AVPlayer?
init(media: MediaType, isPresented: Binding<Bool>, isPlaying: Binding<Bool>, player: AVPlayer?) { init(media: MediaType, isPresented: Binding<Bool>, isPlaying: Binding<Bool>, player: AVPlayer?) {
self.media = media self.media = media
self._isPresented = isPresented self._isPresented = isPresented
self._isPlaying = isPlaying self._isPlaying = isPlaying
if let player = player { self.player = player
self._player = State(initialValue: player)
}
} }
var body: some View { var body: some View {
ZStack { ZStack {
Color.black.edgesIgnoringSafeArea(.all) Color.black.edgesIgnoringSafeArea(.all)
// Media content
ZStack { ZStack {
switch media { switch media {
case .image(let uiImage): case .image(let uiImage):
@ -216,25 +202,21 @@ private struct FullscreenMediaView: View {
} }
} }
case .video(let url, _): case .video(_, _):
VideoPlayerView(url: url, isPlaying: $isPlaying) if let player = player {
.frame(maxWidth: .infinity, maxHeight: .infinity) CustomVideoPlayer(player: player)
.onTapGesture { .onAppear {
withAnimation { player.play()
showControls.toggle() isPlaying = true
} }
} .onDisappear {
.overlay( player.pause()
showControls ? VideoControls( isPlaying = false
isPlaying: $isPlaying, }
onClose: { isPresented = false } }
) : nil
)
} }
} }
.frame(maxWidth: .infinity, maxHeight: .infinity)
// Close button (always visible)
VStack { VStack {
HStack { HStack {
Button(action: { isPresented = false }) { Button(action: { isPresented = false }) {
@ -251,42 +233,22 @@ private struct FullscreenMediaView: View {
Spacer() Spacer()
} }
} }
.onAppear {
if case .video = media {
if isPlaying {
// player?.play()
}
}
}
.onDisappear { .onDisappear {
if case .video = media { player?.pause()
// player?.pause()
// player?.replaceCurrentItem(with: nil)
// player = nil
}
} }
} }
} }
// MARK: - Video Controls // MARK: - Video Player View
private struct VideoControls: View {
@Binding var isPlaying: Bool
let onClose: () -> Void
var body: some View {
// Empty view - no controls shown
EmptyView()
}
}
// MARK: - Video Player with Dynamic Aspect Ratio
struct VideoPlayerView: UIViewRepresentable { struct VideoPlayerView: UIViewRepresentable {
let url: URL let url: URL
@Binding var isPlaying: Bool @Binding var isPlaying: Bool
@Binding var player: AVPlayer?
func makeUIView(context: Context) -> PlayerView { func makeUIView(context: Context) -> PlayerView {
let view = PlayerView() let view = PlayerView()
view.setupPlayer(url: url) let player = view.setupPlayer(url: url)
self.player = player
return view return view
} }
@ -299,39 +261,56 @@ struct VideoPlayerView: UIViewRepresentable {
} }
} }
// MARK: - Custom Video Player
@available(iOS 14.0, *)
struct CustomVideoPlayer: UIViewControllerRepresentable {
let player: AVPlayer
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
controller.videoGravity = .resizeAspect
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
uiViewController.player = player
}
}
// MARK: - Player View
class PlayerView: UIView { class PlayerView: UIView {
private var player: AVPlayer? private var player: AVPlayer?
private var playerLayer: AVPlayerLayer? private var playerLayer: AVPlayerLayer?
private var playerItem: AVPlayerItem? private var playerItem: AVPlayerItem?
private var playerItemObserver: NSKeyValueObservation? private var playerItemObserver: NSKeyValueObservation?
func setupPlayer(url: URL) { @discardableResult
// Clean up existing resources func setupPlayer(url: URL) -> AVPlayer {
cleanup() cleanup()
// Create new player
let asset = AVAsset(url: url) let asset = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: asset) let playerItem = AVPlayerItem(asset: asset)
self.playerItem = playerItem self.playerItem = playerItem
player = AVPlayer(playerItem: playerItem) player = AVPlayer(playerItem: playerItem)
// Setup player layer
let playerLayer = AVPlayerLayer(player: player) let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = .resizeAspect playerLayer.videoGravity = .resizeAspect
layer.addSublayer(playerLayer) layer.addSublayer(playerLayer)
self.playerLayer = playerLayer self.playerLayer = playerLayer
// Layout
playerLayer.frame = bounds playerLayer.frame = bounds
// Add observer for video end
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, self,
selector: #selector(playerItemDidReachEnd), selector: #selector(playerItemDidReachEnd),
name: .AVPlayerItemDidPlayToEndTime, name: .AVPlayerItemDidPlayToEndTime,
object: playerItem object: playerItem
) )
return player!
} }
func play() { func play() {
@ -343,21 +322,17 @@ class PlayerView: UIView {
} }
private func cleanup() { private func cleanup() {
// Remove observers
if let playerItem = playerItem { if let playerItem = playerItem {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: playerItem) NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: playerItem)
} }
// Pause and clean up player
player?.pause() player?.pause()
player?.replaceCurrentItem(with: nil) player?.replaceCurrentItem(with: nil)
player = nil player = nil
// Remove player layer
playerLayer?.removeFromSuperlayer() playerLayer?.removeFromSuperlayer()
playerLayer = nil playerLayer = nil
// Release player item
playerItem?.cancelPendingSeeks() playerItem?.cancelPendingSeeks()
playerItem?.asset.cancelLoading() playerItem?.asset.cancelLoading()
playerItem = nil playerItem = nil
@ -376,25 +351,4 @@ class PlayerView: UIView {
deinit { deinit {
cleanup() cleanup()
} }
} }
// MARK: - Preview
struct BlindOutcomeView_Previews: PreviewProvider {
static var previews: some View {
// Preview with image and details
BlindOutcomeView(
media: .image(UIImage(systemName: "photo")!),
time: "2:30",
description: "This is a sample description for the preview. It shows how the text will wrap and display below the media content."
)
// Preview with video and details
if let url = URL(string: "https://example.com/sample.mp4") {
BlindOutcomeView(
media: .video(url, nil),
time: "1:45",
description: "Video content with time and description"
)
}
}
}

View File

@ -22,7 +22,7 @@ struct JoinModal: View {
// IP Image peeking from top // IP Image peeking from top
HStack { HStack {
// Make sure you have an image named "IP" in your assets // Make sure you have an image named "IP" in your assets
SVGImage(svgName: "IP1") SVGImageHtml(svgName: "IP1")
.frame(width: 116, height: 65) .frame(width: 116, height: 65)
.offset(x: 30) .offset(x: 30)
Spacer() Spacer()