feat: 清空播放器
This commit is contained in:
parent
c221f91412
commit
0670c686d2
@ -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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user