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 os.log
/// A view that displays either an image or a video with fullscreen support
struct BlindOutcomeView: View {
let media: MediaType
let time: String?
@ -12,6 +11,7 @@ struct BlindOutcomeView: View {
@State private var isPlaying = false
@State private var showControls = true
@State private var showIPListModal = false
@State private var player: AVPlayer?
init(media: MediaType, time: String? = nil, description: String? = nil) {
self.media = media
@ -28,7 +28,6 @@ struct BlindOutcomeView: View {
//
HStack {
Button(action: {
//
presentationMode.wrappedValue.dismiss()
}) {
HStack(spacing: 4) {
@ -47,7 +46,6 @@ struct BlindOutcomeView: View {
Spacer()
//
HStack(spacing: 4) {
Image(systemName: "chevron.left")
.opacity(0)
@ -56,7 +54,7 @@ struct BlindOutcomeView: View {
}
.padding(.vertical, 12)
.background(Color.themeTextWhiteSecondary)
.zIndex(1) //
.zIndex(1)
Spacer()
.frame(height: 30)
@ -65,7 +63,6 @@ struct BlindOutcomeView: View {
GeometryReader { geometry in
VStack(spacing: 16) {
ZStack {
//
RoundedRectangle(cornerRadius: 12)
.fill(Color.white)
.shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2)
@ -86,48 +83,41 @@ struct BlindOutcomeView: View {
}
case .video(let url, _):
VideoPlayerView(url: url, isPlaying: $isPlaying)
VideoPlayerView(url: url, isPlaying: $isPlaying, player: $player)
.frame(width: UIScreen.main.bounds.width - 40)
.background(Color.clear)
.cornerRadius(10)
.clipped()
.onAppear {
// Auto-play the video when it appears
isPlaying = true
}
.onDisappear {
isPlaying = false
player?.pause()
}
.onTapGesture {
withAnimation {
showControls.toggle()
}
}
.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) {
Text("Description")
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.themeTextMessageMain)
Text(description)
.font(.system(size: 12))
.foregroundColor(Color.themeTextMessageMain)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 12)
.padding(.bottom, 12)
if let description = description, !description.isEmpty {
VStack(alignment: .leading, spacing: 2) {
Text("Description")
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.themeTextMessageMain)
Text(description)
.font(.system(size: 12))
.foregroundColor(Color.themeTextMessageMain)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 12)
.padding(.bottom, 12)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.top, 8)
}
@ -136,12 +126,13 @@ struct BlindOutcomeView: View {
.padding(.bottom, 20)
}
.padding(.horizontal)
Spacer()
// Button at bottom
VStack {
Spacer()
Button(action: {
// video
if case .video = media {
withAnimation {
showIPListModal = true
@ -162,22 +153,20 @@ struct BlindOutcomeView: View {
}
.padding(.bottom, 20)
}
.onDisappear {
// Clean up video player when view disappears
if case .video = media {
isPlaying = false
}
}
}
.navigationBarHidden(true) //
.navigationBarBackButtonHidden(true) //
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.statusBar(hidden: isFullscreen)
}
.navigationViewStyle(StackNavigationViewStyle()) // iPad
.navigationBarHidden(true) //
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarHidden(true)
.overlay(
JoinModal(isPresented: $showIPListModal)
)
.onDisappear {
player?.pause()
player = nil
}
}
}
@ -187,22 +176,19 @@ private struct FullscreenMediaView: View {
@Binding var isPresented: Bool
@Binding var isPlaying: Bool
@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?) {
self.media = media
self._isPresented = isPresented
self._isPlaying = isPlaying
if let player = player {
self._player = State(initialValue: player)
}
self.player = player
}
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
// Media content
ZStack {
switch media {
case .image(let uiImage):
@ -216,25 +202,21 @@ private struct FullscreenMediaView: View {
}
}
case .video(let url, _):
VideoPlayerView(url: url, isPlaying: $isPlaying)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
withAnimation {
showControls.toggle()
case .video(_, _):
if let player = player {
CustomVideoPlayer(player: player)
.onAppear {
player.play()
isPlaying = true
}
}
.overlay(
showControls ? VideoControls(
isPlaying: $isPlaying,
onClose: { isPresented = false }
) : nil
)
.onDisappear {
player.pause()
isPlaying = false
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
// Close button (always visible)
VStack {
HStack {
Button(action: { isPresented = false }) {
@ -251,42 +233,22 @@ private struct FullscreenMediaView: View {
Spacer()
}
}
.onAppear {
if case .video = media {
if isPlaying {
// player?.play()
}
}
}
.onDisappear {
if case .video = media {
// player?.pause()
// player?.replaceCurrentItem(with: nil)
// player = nil
}
player?.pause()
}
}
}
// MARK: - Video Controls
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
// MARK: - Video Player View
struct VideoPlayerView: UIViewRepresentable {
let url: URL
@Binding var isPlaying: Bool
@Binding var player: AVPlayer?
func makeUIView(context: Context) -> PlayerView {
let view = PlayerView()
view.setupPlayer(url: url)
let player = view.setupPlayer(url: url)
self.player = player
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 {
private var player: AVPlayer?
private var playerLayer: AVPlayerLayer?
private var playerItem: AVPlayerItem?
private var playerItemObserver: NSKeyValueObservation?
func setupPlayer(url: URL) {
// Clean up existing resources
@discardableResult
func setupPlayer(url: URL) -> AVPlayer {
cleanup()
// Create new player
let asset = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
self.playerItem = playerItem
player = AVPlayer(playerItem: playerItem)
// Setup player layer
let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = .resizeAspect
layer.addSublayer(playerLayer)
self.playerLayer = playerLayer
// Layout
playerLayer.frame = bounds
// Add observer for video end
NotificationCenter.default.addObserver(
self,
selector: #selector(playerItemDidReachEnd),
name: .AVPlayerItemDidPlayToEndTime,
object: playerItem
)
return player!
}
func play() {
@ -343,21 +322,17 @@ class PlayerView: UIView {
}
private func cleanup() {
// Remove observers
if let playerItem = playerItem {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: playerItem)
}
// Pause and clean up player
player?.pause()
player?.replaceCurrentItem(with: nil)
player = nil
// Remove player layer
playerLayer?.removeFromSuperlayer()
playerLayer = nil
// Release player item
playerItem?.cancelPendingSeeks()
playerItem?.asset.cancelLoading()
playerItem = nil
@ -376,25 +351,4 @@ class PlayerView: UIView {
deinit {
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
HStack {
// Make sure you have an image named "IP" in your assets
SVGImage(svgName: "IP1")
SVGImageHtml(svgName: "IP1")
.frame(width: 116, height: 65)
.offset(x: 30)
Spacer()