Commit b593bc69 authored by Alex朱枝文's avatar Alex朱枝文

播放器优化生命周期,优化播放器分配、优化小窗

parent 65cd18a7
......@@ -277,7 +277,10 @@ extension YHLifeViewController: UICollectionViewDelegate, UICollectionViewDataSo
return
}
let item = self.viewModel.liveArr[indexPath.row]
YHPlayerManager.shared.enterLive(from: nil, id: item.id, url: item.pull_url, title: item.live_title, roomId: item.room_id)
// YHPlayerManager.shared.enterLive(from: nil, id: item.id, url: item.pull_url, title: item.live_title, roomId: item.room_id)
//let url = "https://pull-flv-l13.douyincdn.com/stage/stream-116307521507688888_ld5.flv?expire=1733728207&sign=cef0df720ef0dbe3126675d72dcacec2&major_anchor_level=common&abr_pts=-800&_session_id=037-2024120215100750CB84D802B8201D3D81.1733123408338.51633&rsi=1"
let playbackInfo = YHPlayerManager.PlaybackInfo(id: item.id, url: item.pull_url, title: item.live_title, roomId: item.room_id, isLive: true, scene: .fullscreen)
YHPlayerManager.shared.enterLive(from: nil, playbackInfo: playbackInfo)
}
}
......
......@@ -141,7 +141,8 @@ extension YHHomeBannerView: FSPagerViewDataSource, FSPagerViewDelegate {
// // TODO: - alex测试
// if index == 1 {
// let cell: YHHomeBannerCollectionViewCell? = pagerView.cellForItem(at: index) as? YHHomeBannerCollectionViewCell
// YHPlayerManager.shared.enterLive(from: cell?.bannerImagV, id: 23, url: "https://pull-flv-l6.douyincdn.com/stage/stream-116295918585905183.flv?k=e21f1ae1e7591521&t=1733551151&major_anchor_level=common&abr_pts=-800&_session_id=037-202411301359108030CAEAC1F742805E6D.1732946351732.18942&rsi=1", title: nil, roomId: nil, type: .secondary)
// let playbackInfo = YHPlayerManager.PlaybackInfo(id: 40, url: "https://pull-flv-l13.douyincdn.com/stage/stream-116307521507688888_ld5.flv?expire=1733728207&sign=cef0df720ef0dbe3126675d72dcacec2&major_anchor_level=common&abr_pts=-800&_session_id=037-2024120215100750CB84D802B8201D3D81.1733123408338.51633&rsi=1", title: nil, roomId: nil, isLive: true, scene: .fullscreen)
// YHPlayerManager.shared.enterLive(from: cell?.bannerImagV, playbackInfo: playbackInfo)
// return
// }
// // TODO: - alex测试
......@@ -223,16 +224,19 @@ extension YHHomeBannerView: FSPagerViewDataSource, FSPagerViewDelegate {
// video_url 视频链接
// recorded_cate_id 录播分类id
let cell: YHHomeBannerCollectionViewCell? = pagerView.cellForItem(at: index) as? YHHomeBannerCollectionViewCell
YHPlayerManager.shared.enterLive(from: cell?.bannerImagV, id: model.live_id, url: model.live_pull_url, title: nil, roomId: nil, type: .secondary)
let playbackInfo = YHPlayerManager.PlaybackInfo(id: model.live_id, url: model.live_pull_url, title: nil, roomId: nil, isLive: true, scene: .fullscreen)
YHPlayerManager.shared.enterLive(from: cell?.bannerImagV, playbackInfo: playbackInfo)
printLog("跳转直播")
case 101://录播
printLog("跳转录播")
let cell: YHHomeBannerCollectionViewCell? = pagerView.cellForItem(at: index) as? YHHomeBannerCollectionViewCell
YHPlayerManager.shared.enterVOD(from: cell?.bannerImagV, id: model.live_id, url: model.video_url, title: nil, type: .secondary)
let playbackInfo = YHPlayerManager.PlaybackInfo(id: model.live_id, url: model.video_url, title: nil, roomId: nil, isLive: false, scene: .fullscreen)
YHPlayerManager.shared.enterVOD(from: cell?.bannerImagV, playbackInfo: playbackInfo)
case 102://图片直播
printLog("跳转录播")
let cell: YHHomeBannerCollectionViewCell? = pagerView.cellForItem(at: index) as? YHHomeBannerCollectionViewCell
YHPlayerManager.shared.enterLive(from: cell?.bannerImagV, id: model.live_id, url: model.live_pull_url, title: nil, roomId: nil, type: .secondary)
let playbackInfo = YHPlayerManager.PlaybackInfo(id: model.live_id, url: model.live_pull_url, title: nil, roomId: nil, isLive: true, scene: .fullscreen)
YHPlayerManager.shared.enterLive(from: cell?.bannerImagV, playbackInfo: playbackInfo)
case 0://0 不需要跳转
printLog("0 不需要跳转")
default:
......@@ -266,7 +270,10 @@ extension YHHomeBannerView: FSPagerViewDataSource, FSPagerViewDelegate {
// // TODO: - alex测试
// if let cell = cell as? YHHomeBannerCollectionViewCell {
// if index == 1 {
// YHPlayerManager.shared.play(url: "https://pull-flv-l11.douyincdn.com/thirdgame/stream-404525958790382412.flv?expire=1733554587&sign=d1e9f927e20f4a3fb4e2dd2a2712e256&major_anchor_level=common&abr_pts=-800&_session_id=037-20241130145626DBDEB00EB11CB388DD95.1732949787574.66743&rsi=1", inView: cell.bannerImagV, title: nil, type: .secondary)
//
// let playbackInfo = YHPlayerManager.PlaybackInfo(id: 40, url: "https://pull-flv-l13.douyincdn.com/stage/stream-116307521507688888_ld5.flv?expire=1733728207&sign=cef0df720ef0dbe3126675d72dcacec2&major_anchor_level=common&abr_pts=-800&_session_id=037-2024120215100750CB84D802B8201D3D81.1733123408338.51633&rsi=1", title: nil, roomId: nil, isLive: true, scene: .banner)
// YHPlayerManager.shared.enterBanner(playbackInfo: playbackInfo, inView: cell.bannerImagV)
//
// } else {
// let player = YHPlayerManager.shared.getPlayer(.secondary)
// player?.setPlayView(nil)
......@@ -276,8 +283,10 @@ extension YHHomeBannerView: FSPagerViewDataSource, FSPagerViewDelegate {
// // TODO: - alex测试
if model.skip_url.isEmpty == false {
if let cell = cell as? YHHomeBannerCollectionViewCell {
if model.skip_type == 100 {
if model.skip_type == 100 || model.skip_type == 102 {
YHPlayerManager.shared.play(url: model.live_pull_url, inView: cell.bannerImagV, title: nil, type: .secondary)
let playbackInfo = YHPlayerManager.PlaybackInfo(id: model.live_id, url: model.live_pull_url, title: nil, roomId: nil, isLive: true, scene: .banner)
YHPlayerManager.shared.enterBanner(playbackInfo: playbackInfo, inView: cell.bannerImagV)
} else {
let player = YHPlayerManager.shared.getPlayer(.secondary)
player?.setPlayView(nil)
......@@ -302,7 +311,7 @@ extension YHHomeBannerView: FSPagerViewDataSource, FSPagerViewDelegate {
// }
// // TODO: - alex测试
if model.skip_url.isEmpty == false {
if model.skip_type == 100 {
if model.skip_type == 100 || model.skip_type == 102 {
YHPlayerManager.shared.stop(type: .secondary)
}
}
......
......@@ -94,7 +94,8 @@ extension YHSelectLookView: UICollectionViewDelegate, UICollectionViewDataSource
return
}
let item = items[indexPath.row]
YHPlayerManager.shared.enterLive(from: nil, id: item.id, url: item.pull_url, title: item.live_title, roomId: item.room_id)
let playbackInfo = YHPlayerManager.PlaybackInfo(id: item.id, url: item.pull_url, title: item.live_title, roomId: item.room_id, isLive: true, scene: .fullscreen)
YHPlayerManager.shared.enterLive(from: nil, playbackInfo: playbackInfo)
}
}
......
......@@ -31,9 +31,17 @@ class YHPlayer {
weak var currentPlayView: UIView?
private(set) var currentTitle: String?
var isMuted: Bool {
get { playerKit.getMute() }
set { playerKit.mute(newValue) }
}
init(type: YHPlayerType, playerKit: AgoraRtcMediaPlayerProtocol) {
self.type = type
self.playerKit = playerKit
// 基础设置
playerKit.setLoopCount(-1) // 循环播放
}
func setPlayView(_ view: UIView?) {
......@@ -47,7 +55,10 @@ class YHPlayer {
let mediaSource = AgoraMediaSource()
mediaSource.url = url
mediaSource.autoPlay = true
playerKit.open(with: mediaSource)
let result = playerKit.open(with: mediaSource)
if result == 0 {
playerKit.play()
}
}
func stop() {
......@@ -65,6 +76,17 @@ class YHPlayer {
playerKit.play()
}
func reset() {
stop()
setPlayView(nil)
delegate = nil
}
func releasePlayer() {
reset()
//playerKit.destroy()
}
func getPosition() -> Int {
return playerKit.getPosition()
}
......
......@@ -12,195 +12,212 @@ import UIKit
// MARK: - 播放器管理中心
class YHPlayerManager: NSObject {
// 播放器状态信息
// MARK: - Types
enum PlaybackScene: Int {
case fullscreen // 直播间/点播页
case floating // 小窗
case banner // banner
}
struct PlaybackInfo: Equatable {
let id: Int
let url: String?
let title: String?
let roomId: String?
let isLive: Bool
init(id: Int, url: String? = nil, title: String? = nil, roomId: String? = nil, isLive: Bool) {
var scene: PlaybackScene
var playerType: YHPlayerType
init(id: Int, url: String? = nil, title: String? = nil, roomId: String? = nil, isLive: Bool, scene: PlaybackScene = .fullscreen) {
self.id = id
self.url = url
self.title = title
self.roomId = roomId
self.isLive = isLive
self.scene = scene
playerType = .main
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id && lhs.isLive == rhs.isLive
}
}
// MARK: - Properties
static let shared = YHPlayerManager()
// 核心组件
private var agoraKit: AgoraRtcEngineKit!
private var players: [YHPlayerType: YHPlayer] = [:]
private var activePlayers: [YHPlayerType: YHPlayer] = [:]
private var currentPlaybackInfo: [YHPlayerType: PlaybackInfo] = [:]
private var floatingWindow: YHFloatingWindow?
// 转场动画相关
private var transitionSourceView: UIView?
// MARK: - Initialization
override private init() {
super.init()
setupAgoraKit()
setupPlayers()
}
// MARK: - Setup Methods
private func setupAgoraKit() {
let config = AgoraRtcEngineConfig()
config.appId = YhConstant.AgoraRtcKit.appId
config.areaCode = .global
config.channelProfile = .liveBroadcasting
agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
// 基础配置
agoraKit.enableVideo()
}
private func setupPlayers() {
// 创建主播放器
if let mainPlayerKit = agoraKit.createMediaPlayer(with: self) {
players[.main] = YHPlayer(type: .main, playerKit: mainPlayerKit)
}
// MARK: - Basic Playback Control
// 创建第二播放器
if let secondaryPlayerKit = agoraKit.createMediaPlayer(with: self) {
players[.secondary] = YHPlayer(type: .secondary, playerKit: secondaryPlayerKit)
func play(url: String, inView view: UIView? = nil, title: String? = nil, type: YHPlayerType = .main) {
let player = player(for: type)
if let view = view {
player.setPlayView(view)
}
player.play(url: url, title: title)
}
// MARK: - Public Methods
// 获取播放器实例
func getPlayer(_ type: YHPlayerType) -> YHPlayer? {
return players[type]
func pause(type: YHPlayerType = .main) {
activePlayers[type]?.pause()
}
// 播放控制
func play(url: String, inView view: UIView? = nil, title: String? = nil, type: YHPlayerType = .main) {
if let player = players[type] {
// 如果提供了视图,则设置播放视图
if let view = view {
player.setPlayView(view)
}
player.play(url: url, title: title)
}
}
func play(playbackInfo: PlaybackInfo, inView view: UIView? = nil, type: YHPlayerType = .main) {
if let player = players[type] {
// 如果提供了视图,则设置播放视图
currentPlaybackInfo[type] = playbackInfo
if let view = view {
player.setPlayView(view)
}
if let url = playbackInfo.url {
player.play(url: url, title: playbackInfo.title)
}
}
func resume(type: YHPlayerType = .main) {
activePlayers[type]?.resume()
}
func resume(type: YHPlayerType = .main) {
players[type]?.resume()
func stop(type: YHPlayerType = .main) {
activePlayers[type]?.stop()
}
func pause(type: YHPlayerType = .main) {
players[type]?.pause()
func setPlayView(_ view: UIView?, type: YHPlayerType = .main) {
activePlayers[type]?.setPlayView(view)
}
func stop(type: YHPlayerType = .main) {
players[type]?.stop()
func getCurrentPlayer(type: YHPlayerType = .main) -> YHPlayer? {
return activePlayers[type]
}
func stopAllPlayers() {
players.values.forEach { $0.stop() }
func setMute(_ muted: Bool, type: YHPlayerType = .main) {
activePlayers[type]?.isMuted = muted
}
// 视图控制
func setPlayView(_ view: UIView?, type: YHPlayerType = .main) {
players[type]?.setPlayView(view)
func getPlayer(_ type: YHPlayerType) -> YHPlayer? {
return activePlayers[type]
}
// MARK: - 资源管理
// MARK: - Player Management
func destroy() {
stopAllPlayers()
players.removeAll()
exitFloating()
AgoraRtcEngineKit.destroy()
private func player(for type: YHPlayerType) -> YHPlayer {
if let existingPlayer = activePlayers[type] {
return existingPlayer
}
let newPlayer = createPlayer(for: type)
activePlayers[type] = newPlayer
return newPlayer
}
}
extension YHPlayerManager {
// MARK: - 场景切换
func enterVOD(from sourceView: UIView?, playbackInfo: PlaybackInfo, type: YHPlayerType = .main) {
let playerVC = YHVODPlayerViewController(id: playbackInfo.id, url: playbackInfo.url, title: playbackInfo.title)
// 保存播放信息
currentPlaybackInfo[type] = playbackInfo
if let player = players[type] {
player.delegate = playerVC
playerVC.player = player
private func createPlayer(for type: YHPlayerType) -> YHPlayer {
guard let playerKit = agoraKit.createMediaPlayer(with: self) else {
fatalError("Failed to create media player")
}
playerVC.playbackInfo = playbackInfo
present(playerVC, from: sourceView)
let player = YHPlayer(type: type, playerKit: playerKit)
return player
}
func enterLive(from sourceView: UIView?, playbackInfo: PlaybackInfo, type: YHPlayerType = .main) {
// 保存播放信息
currentPlaybackInfo[type] = playbackInfo
let playerVC = YHLivePlayerViewController(id: playbackInfo.id, url: playbackInfo.url, title: playbackInfo.title, roomId: playbackInfo.roomId)
playerVC.playbackInfo = playbackInfo
if let player = players[type] {
player.delegate = playerVC
playerVC.player = player
private func releasePlayer(_ type: YHPlayerType) {
guard let player = activePlayers[type] else { return }
player.stop()
player.releasePlayer()
activePlayers.removeValue(forKey: type)
currentPlaybackInfo.removeValue(forKey: type)
}
private func determinePlayerType(for scene: PlaybackScene) -> YHPlayerType {
switch scene {
case .banner:
return .secondary
case .fullscreen, .floating:
return .main
}
present(playerVC, from: sourceView)
}
func enterVOD(from sourceView: UIView?, id: Int, url: String, title: String?, type: YHPlayerType = .main) {
let playerVC = YHVODPlayerViewController(id: id, url: url, title: title)
// 保存播放信息
let playbackInfo = PlaybackInfo(id: id, url: url, title: title, isLive: false)
playerVC.playbackInfo = playbackInfo
currentPlaybackInfo[type] = playbackInfo
if let player = players[type] {
player.delegate = playerVC
playerVC.player = player
// MARK: - Scene Management
func enterVOD(from sourceView: UIView?, playbackInfo: PlaybackInfo) {
let playerType = determinePlayerType(for: .fullscreen)
var updatedInfo = playbackInfo
updatedInfo.scene = .fullscreen
updatedInfo.playerType = playerType
// 如果是从banner跳转,需要转移播放进度
var startPosition: Int = 0
if playbackInfo.scene == .banner,
let bannerPlayer = activePlayers[.secondary] {
startPosition = bannerPlayer.getPosition()
releasePlayer(.secondary)
}
let playerVC = YHVODPlayerViewController(id: playbackInfo.id, url: playbackInfo.url, title: playbackInfo.title)
currentPlaybackInfo[playerType] = updatedInfo
let player = player(for: playerType)
player.delegate = playerVC
playerVC.player = player
playerVC.playbackInfo = updatedInfo
playerVC.startPosition = startPosition
// 关闭小窗
exitFloating()
present(playerVC, from: sourceView)
}
func enterLive(from sourceView: UIView?, id: Int, url: String? = nil, title: String? = nil, roomId: String? = nil, type: YHPlayerType = .main) {
// 保存播放信息
let playbackInfo = PlaybackInfo(id: id, url: url, title: title, roomId: roomId, isLive: true)
currentPlaybackInfo[type] = playbackInfo
let playerVC = YHLivePlayerViewController(id: id, url: url, title: title, roomId: roomId)
playerVC.playbackInfo = playbackInfo
if let player = players[type] {
player.delegate = playerVC
playerVC.player = player
func enterLive(from sourceView: UIView?, playbackInfo: PlaybackInfo) {
let playerType = determinePlayerType(for: .fullscreen)
var updatedInfo = playbackInfo
updatedInfo.scene = .fullscreen
updatedInfo.playerType = playerType
// 如果是从banner跳转,释放副播放器
if playbackInfo.scene == .banner {
releasePlayer(.secondary)
}
let playerVC = YHLivePlayerViewController(id: playbackInfo.id, url: playbackInfo.url, title: playbackInfo.title, roomId: playbackInfo.roomId)
currentPlaybackInfo[playerType] = updatedInfo
let player = player(for: playerType)
player.delegate = playerVC
playerVC.player = player
playerVC.playbackInfo = updatedInfo
// 关闭小窗
exitFloating()
present(playerVC, from: sourceView)
}
func enterFloating(from viewController: UIViewController? = nil, playbackInfo: PlaybackInfo, type: YHPlayerType = .main) {
guard let window = UIApplication.shared.keyWindow,
let player = players[type] else { return }
currentPlaybackInfo[type] = playbackInfo
func enterFloating(from viewController: UIViewController? = nil, playbackInfo: PlaybackInfo) {
guard let window = UIApplication.shared.keyWindow else { return }
let playerType = determinePlayerType(for: .floating)
let player = player(for: playerType)
var updatedInfo = playbackInfo
updatedInfo.scene = .floating
updatedInfo.playerType = playerType
currentPlaybackInfo[playerType] = updatedInfo
if let url = playbackInfo.url {
player.play(url: url, title: playbackInfo.title)
}
// 获取当前播放视图的截图和位置
if let sourceView = player.currentPlayView,
let sourceSuperview = sourceView.superview {
......@@ -210,7 +227,7 @@ extension YHPlayerManager {
// 创建浮窗
let floatingWindow = YHFloatingWindow()
floatingWindow.playbackInfo = playbackInfo
floatingWindow.playbackInfo = updatedInfo
floatingWindow.delegate = self
floatingWindow.player = player
self.floatingWindow = floatingWindow
......@@ -224,61 +241,59 @@ extension YHPlayerManager {
// 执行动画
let showFloatingWindow = { [weak self] in
guard let self = self else { return }
// 动画过渡
UIView.animate(withDuration: 0.3, animations: {
snapshotView.frame = targetFrame
}, completion: { _ in
// 移除截图视图
snapshotView.removeFromSuperview()
// 显示真正的浮窗
floatingWindow.show(in: window)
// 切换播放视图
player.setPlayView(floatingWindow.contentView)
})
}
// 如果有viewController需要关闭
if let viewController = viewController {
// 先执行动画,动画完成后再关闭页面
showFloatingWindow()
viewController.dismiss(animated: false)
} else {
showFloatingWindow()
}
} else {
// 创建浮窗
// 直接创建浮窗
let floatingWindow = YHFloatingWindow()
floatingWindow.delegate = self
floatingWindow.player = players[type]
floatingWindow.player = player
floatingWindow.playbackInfo = updatedInfo
self.floatingWindow = floatingWindow
let showFloatingWindow = { [weak self] in
guard let self = self else { return }
// 显示浮窗
floatingWindow.show(in: window)
// 切换播放视图
if let player = self.players[type] {
player.setPlayView(floatingWindow.contentView)
}
player.setPlayView(floatingWindow.contentView)
}
// 如果有viewController需要关闭,先关闭再显示浮窗
if let viewController = viewController {
viewController.dismiss(animated: true) {
showFloatingWindow()
}
} else {
// 直接显示浮窗
showFloatingWindow()
}
}
}
// 退出浮窗
func enterBanner(playbackInfo: PlaybackInfo, inView view: UIView) {
let playerType = determinePlayerType(for: .banner)
var updatedInfo = playbackInfo
updatedInfo.scene = .banner
updatedInfo.playerType = playerType
currentPlaybackInfo[playerType] = updatedInfo
let player = player(for: playerType)
player.setPlayView(view)
if let url = playbackInfo.url {
player.play(url: url, title: playbackInfo.title)
}
}
func exitFloating() {
floatingWindow?.dismiss()
floatingWindow = nil
......@@ -289,13 +304,38 @@ extension YHPlayerManager {
playerVC.transitioningDelegate = self
transitionSourceView = sourceView
if let topVC = UIApplication.shared.keyWindow?.rootViewController?.topMostViewController() {
if let topVC = UIApplication.shared.keyWindow?.rootViewController?.topMostViewController() {
topVC.present(playerVC, animated: true) { [weak self] in
self?.exitFloating()
self?.transitionSourceView = nil
}
}
}
// MARK: - Resource Management
func cleanupOnExit() {
// 停止所有播放
activePlayers.forEach { $0.value.stop() }
// 释放副播放器
releasePlayer(.secondary)
// 清理播放信息
currentPlaybackInfo.removeAll()
floatingWindow?.dismiss()
floatingWindow = nil
// 重置主播放器
if let mainPlayer = activePlayers[.main] {
mainPlayer.reset()
}
}
deinit {
activePlayers.forEach { $0.value.releasePlayer() }
activePlayers.removeAll()
AgoraRtcEngineKit.destroy()
}
}
// MARK: - AgoraRtcEngineDelegate
......@@ -316,28 +356,23 @@ extension YHPlayerManager: AgoraRtcMediaPlayerDelegate {
func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol,
didChangedTo state: AgoraMediaPlayerState,
reason: AgoraMediaPlayerReason) {
printLog("Player state changed to: \(state.rawValue), error: \(reason.rawValue)")
// 找到对应的播放器并通知代理
if let player = players.values.first(where: { $0.playerKit === playerKit }) {
if let player = activePlayers.values.first(where: { $0.playerKit === playerKit }) {
player.delegate?.player(player, didChangedToState: state, reason: reason)
}
}
func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol,
didChangedTo position: Int) {
// 处理播放进度更新
if let player = players.values.first(where: { $0.playerKit === playerKit }) {
if let player = activePlayers.values.first(where: { $0.playerKit === playerKit }) {
player.delegate?.player(player, didChangedToPosition: position)
}
}
func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol,
didReceiveVideoSize size: CGSize) {
if let player = players.values.first(where: { $0.playerKit === playerKit }) {
if let player = activePlayers.values.first(where: { $0.playerKit === playerKit }) {
player.delegate?.player(player, didReceiveVideoSize: size)
// 如果是主播放器且在浮窗模式,更新浮窗尺寸
}
// 更新浮窗视频尺寸
if floatingWindow?.player?.playerKit === playerKit {
floatingWindow?.setVideoSize(size)
......@@ -348,29 +383,28 @@ extension YHPlayerManager: AgoraRtcMediaPlayerDelegate {
// MARK: - YHFloatingWindowDelegate
extension YHPlayerManager: YHFloatingWindowDelegate {
// 通过PlaybackInfo查找对应的PlayerType
private func findPlayerType(for playbackInfo: PlaybackInfo) -> YHPlayerType? {
return currentPlaybackInfo.first(where: { $0.value == playbackInfo })?.key
}
func floatingWindowDidTap(_ window: YHFloatingWindow) {
// 点击浮窗时进入直播间
guard let playbackInfo = window.playbackInfo else {
return
}
guard let type = findPlayerType(for: playbackInfo) else {
return
}
guard let playbackInfo = window.playbackInfo else { return }
if playbackInfo.isLive {
enterLive(from: window.contentView, playbackInfo: playbackInfo, type: type)
enterLive(from: window.contentView, playbackInfo: playbackInfo)
} else {
enterVOD(from: window.contentView, playbackInfo: playbackInfo, type: type)
enterVOD(from: window.contentView, playbackInfo: playbackInfo)
}
}
func floatingWindowDidClose(_ window: YHFloatingWindow) {
// 关闭浮窗时停止播放
stopAllPlayers()
guard let playbackInfo = window.playbackInfo,
let playerType = findPlayerType(for: playbackInfo) else {
return
}
let player = player(for: playerType)
player.stop()
floatingWindow = nil
}
......@@ -387,15 +421,15 @@ extension YHPlayerManager: YHFloatingWindowDelegate {
extension YHPlayerManager: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if let sourceView = transitionSourceView {
return YHPlayerTransitionAnimator(type: .zoomIn, sourceView: sourceView)
} else {
return YHPlayerTransitionAnimator(type: .push)
}
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if let sourceView = transitionSourceView {
return YHPlayerTransitionAnimator(type: .zoomOut, sourceView: sourceView)
......@@ -405,6 +439,8 @@ extension YHPlayerManager: UIViewControllerTransitioningDelegate {
}
}
// MARK: - UIViewController Extension
extension UIViewController {
func topMostViewController() -> UIViewController {
if let presented = presentedViewController {
......
......@@ -13,6 +13,7 @@ import AgoraRtcKit
class YHVODPlayerViewController: YHBasePlayerViewController {
// MARK: - Properties
private let vodId: Int
var startPosition: Int = 0
//private let viewModel = YHVideoViewModel()
// MARK: - Initialization
......
......@@ -19,16 +19,16 @@ protocol YHFloatingWindowDelegate: AnyObject {
class YHFloatingWindow: NSObject {
// MARK: - Properties
weak var player: YHPlayer?
weak var delegate: YHFloatingWindowDelegate?
var playbackInfo: YHPlayerManager.PlaybackInfo?
// 视频方向
enum VideoOrientation {
case portrait // 竖屏 9:16
case landscape // 横屏 16:9
var aspectRatio: CGFloat {
switch self {
case .portrait: return 9.0 / 16.0
......@@ -36,32 +36,32 @@ class YHFloatingWindow: NSObject {
}
}
}
// 窗口尺寸
private struct Size {
static let minWidth: CGFloat = 120
static let maxWidth: CGFloat = UIScreen.main.bounds.width
static let minHeight: CGFloat = 67.5 // 16:9
static let maxHeight: CGFloat = UIScreen.main.bounds.height
static let defaultWidth: CGFloat = 150
static let defaultHeight: CGFloat = 84.375 // 16:9
}
private(set) var contentView: UIView
private var containerView: UIView
private var videoOrientation: VideoOrientation = .landscape
// 手势相关
private var initialFrame: CGRect = .zero
private var isResizing: Bool = false
private var initialCenter: CGPoint = .zero
// 缩放相关
private var currentScale: CGFloat = 1.0
private var initialDistance: CGFloat = 0
private let scaleMultiplier: CGFloat = 1.5
// UI组件
private lazy var closeButton: UIButton = {
let button = UIButton(type: .custom)
......@@ -70,86 +70,86 @@ class YHFloatingWindow: NSObject {
button.frame = CGRect(x: 0, y: 0, width: 24, height: 24)
return button
}()
private lazy var closeButtonContainer: UIView = {
let container = UIView(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
container.backgroundColor = .clear
container.addSubview(closeButton)
closeButton.center = CGPoint(x: container.bounds.width/2, y: container.bounds.height/2)
closeButton.center = CGPoint(x: container.bounds.width / 2, y: container.bounds.height / 2)
return container
}()
// MARK: - Initialization
override init() {
containerView = UIView(frame: CGRect(x: 0, y: 0,
width: Size.defaultWidth,
height: Size.defaultHeight))
width: Size.defaultWidth,
height: Size.defaultHeight))
contentView = UIView(frame: containerView.bounds)
super.init()
setupUI()
setupGestures()
}
// MARK: - Setup
private func setupUI() {
containerView.backgroundColor = .black
containerView.layer.cornerRadius = 3
containerView.clipsToBounds = true
containerView.layer.masksToBounds = true
containerView.layer.shadowColor = UIColor.black.cgColor
containerView.layer.shadowOffset = CGSize(width: 0, height: 2)
containerView.layer.shadowRadius = 4
containerView.layer.shadowOpacity = 0.3
containerView.addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
contentView.topAnchor.constraint(equalTo: containerView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
])
containerView.addSubview(closeButtonContainer)
closeButtonContainer.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
closeButtonContainer.topAnchor.constraint(equalTo: containerView.topAnchor),
closeButtonContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
closeButtonContainer.widthAnchor.constraint(equalToConstant: 30),
closeButtonContainer.heightAnchor.constraint(equalToConstant: 30)
closeButtonContainer.heightAnchor.constraint(equalToConstant: 30),
])
}
private func setupGestures() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
tapGesture.delegate = self
containerView.addGestureRecognizer(panGesture)
containerView.addGestureRecognizer(pinchGesture)
containerView.addGestureRecognizer(tapGesture)
}
// MARK: - Public Methods
func calculateInitialFrame() -> CGRect {
let width: CGFloat = Size.defaultWidth
let height: CGFloat = width / videoOrientation.aspectRatio
let x = UIScreen.main.bounds.width - width - 16
let y = UIScreen.main.bounds.height - height - 100
return CGRect(x: x, y: y, width: width, height: height)
}
func show(in window: UIWindow) {
containerView.frame = calculateInitialFrame()
window.addSubview(containerView)
}
func show(in window: UIWindow, at point: CGPoint? = nil) {
if let point = point {
containerView.center = point
......@@ -159,15 +159,15 @@ class YHFloatingWindow: NSObject {
y: window.bounds.height - containerView.bounds.height - 100
)
}
window.addSubview(containerView)
containerView.alpha = 0
UIView.animate(withDuration: 0.3) {
self.containerView.alpha = 1
}
}
func dismiss() {
UIView.animate(withDuration: 0.3, animations: {
self.containerView.alpha = 0
......@@ -175,7 +175,7 @@ class YHFloatingWindow: NSObject {
self.containerView.removeFromSuperview()
}
}
func setVideoSize(_ size: CGSize) {
let orientation: VideoOrientation = size.width > size.height ? .landscape : .portrait
if orientation != videoOrientation {
......@@ -183,13 +183,13 @@ class YHFloatingWindow: NSObject {
updateLayoutForOrientation()
}
}
// MARK: - Private Methods
private func updateLayoutForOrientation() {
let currentWidth = containerView.bounds.width
let newHeight = currentWidth / videoOrientation.aspectRatio
UIView.animate(withDuration: 0.3) {
var frame = self.containerView.frame
frame.size.height = newHeight
......@@ -197,58 +197,18 @@ class YHFloatingWindow: NSObject {
self.delegate?.floatingWindow(self, didChangeSize: frame.size)
}
}
private func snapToNearestSize() {
let currentWidth = containerView.bounds.width
let sizeSteps: [CGFloat] = [
Size.minWidth,
Size.maxWidth * 0.33,
Size.maxWidth * 0.5,
Size.maxWidth * 0.75,
Size.maxWidth
]
var targetWidth = sizeSteps[0]
var minDifference = abs(currentWidth - targetWidth)
for size in sizeSteps {
let difference = abs(currentWidth - size)
if difference < minDifference {
minDifference = difference
targetWidth = size
}
}
let targetHeight = targetWidth / videoOrientation.aspectRatio
let centerX = containerView.center.x
let centerY = containerView.center.y
UIView.animate(withDuration: 0.3,
delay: 0,
options: [.curveEaseOut],
animations: {
var frame = self.containerView.frame
frame.size = CGSize(width: targetWidth, height: targetHeight)
frame.origin.x = centerX - targetWidth / 2
frame.origin.y = centerY - targetHeight / 2
self.containerView.frame = frame
self.delegate?.floatingWindow(self, didChangeSize: frame.size)
})
}
// MARK: - Gesture Handlers
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard !isResizing else { return }
let translation = gesture.translation(in: containerView.superview)
switch gesture.state {
case .began:
initialCenter = containerView.center
case .changed:
var newCenter = CGPoint(
x: initialCenter.x + translation.x,
......@@ -257,137 +217,204 @@ class YHFloatingWindow: NSObject {
newCenter = adjustedPosition(for: newCenter)
containerView.center = newCenter
delegate?.floatingWindow(self, didChangePosition: newCenter)
case .ended:
let velocity = gesture.velocity(in: containerView.superview)
handlePanEndedWithVelocity(velocity)
default:
break
}
}
private func snapToNearestSize() {
guard let superview = containerView.superview else { return }
let currentWidth = containerView.bounds.width
let screenWidth = superview.bounds.width
let screenHeight = superview.bounds.height
// 简化尺寸档位,且确保不超过屏幕宽度的75%
let maxAllowedWidth = min(Size.maxWidth, screenWidth * 0.75)
let sizeSteps: [CGFloat] = [
Size.minWidth,
Size.defaultWidth,
maxAllowedWidth * 0.5,
maxAllowedWidth,
]
// 找到最接近的宽度
let targetWidth = sizeSteps.min(by: { abs($0 - currentWidth) < abs($1 - currentWidth) }) ?? Size.defaultWidth
let targetHeight = targetWidth / videoOrientation.aspectRatio
// 确保高度不超过屏幕高度的75%
let maxAllowedHeight = screenHeight * 0.75
let finalWidth: CGFloat
let finalHeight: CGFloat
if targetHeight > maxAllowedHeight {
finalHeight = maxAllowedHeight
finalWidth = finalHeight * videoOrientation.aspectRatio
} else {
finalWidth = targetWidth
finalHeight = targetHeight
}
UIView.animate(withDuration: 0.3) {
var frame = self.containerView.frame
// 设置新的尺寸
frame.size = CGSize(width: finalWidth, height: finalHeight)
// 计算新的中心点,确保不超出屏幕边界
let centerX = max(finalWidth / 2, min(self.containerView.center.x, screenWidth - finalWidth / 2))
let centerY = max(finalHeight / 2, min(self.containerView.center.y, screenHeight - finalHeight / 2))
// 基于新的中心点设置origin
frame.origin.x = centerX - finalWidth / 2
frame.origin.y = centerY - finalHeight / 2
self.containerView.frame = frame
self.delegate?.floatingWindow(self, didChangeSize: frame.size)
}
}
@objc private func handlePinch(_ gesture: UIPinchGestureRecognizer) {
guard let superview = containerView.superview else { return }
switch gesture.state {
case .began:
isResizing = true
initialFrame = containerView.frame
currentScale = 1.0
let touch1 = gesture.location(ofTouch: 0, in: containerView)
let touch2 = gesture.location(ofTouch: 1, in: containerView)
initialDistance = hypot(touch2.x - touch1.x, touch2.y - touch1.y)
case .changed:
let touch1 = gesture.location(ofTouch: 0, in: containerView)
let touch2 = gesture.location(ofTouch: 1, in: containerView)
let currentDistance = hypot(touch2.x - touch1.x, touch2.y - touch1.y)
let scale = (currentDistance / initialDistance) * scaleMultiplier
let scaleDelta = scale / currentScale
currentScale = scale
let newWidth = initialFrame.width * scaleDelta
let newHeight = newWidth / videoOrientation.aspectRatio
var newFrame = initialFrame
newFrame.size = constrainSize(CGSize(width: newWidth, height: newHeight))
let centerX = containerView.center.x
let centerY = containerView.center.y
newFrame.origin.x = centerX - newFrame.width / 2
newFrame.origin.y = centerY - newFrame.height / 2
CATransaction.begin()
CATransaction.setDisableActions(true)
let scale = gesture.scale
let screenWidth = superview.bounds.width
let screenHeight = superview.bounds.height
// 计算新的尺寸
var newWidth = initialFrame.width * scale
var newHeight = newWidth / videoOrientation.aspectRatio
// 确保不超过屏幕最大限制(75%的屏幕大小)
let maxWidth = screenWidth * 0.75
let maxHeight = screenHeight * 0.75
if newWidth > maxWidth {
newWidth = maxWidth
newHeight = newWidth / videoOrientation.aspectRatio
}
if newHeight > maxHeight {
newHeight = maxHeight
newWidth = newHeight * videoOrientation.aspectRatio
}
// 确保不小于最小尺寸
if newWidth < Size.minWidth {
newWidth = Size.minWidth
newHeight = newWidth / videoOrientation.aspectRatio
}
// 保持中心点不变,但确保不超出屏幕边界
var newCenter = containerView.center
// 约束中心点,确保视图不会超出屏幕
newCenter.x = max(newWidth / 2, min(newCenter.x, screenWidth - newWidth / 2))
newCenter.y = max(newHeight / 2, min(newCenter.y, screenHeight - newHeight / 2))
// 更新frame
var newFrame = containerView.frame
newFrame.size = CGSize(width: newWidth, height: newHeight)
newFrame.origin.x = newCenter.x - newWidth / 2
newFrame.origin.y = newCenter.y - newHeight / 2
containerView.frame = newFrame
CATransaction.commit()
delegate?.floatingWindow(self, didChangeSize: newFrame.size)
case .ended, .cancelled:
isResizing = false
snapToNearestSize()
default:
break
}
}
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: containerView)
if closeButtonContainer.frame.contains(location) {
return
}
delegate?.floatingWindowDidTap(self)
}
@objc private func closeButtonTapped() {
delegate?.floatingWindowDidClose(self)
dismiss()
}
// MARK: - Helper Methods
private func constrainSize(_ size: CGSize) -> CGSize {
var width = size.width
var height = size.height
if width < Size.minWidth {
width = Size.minWidth
} else if width > Size.maxWidth {
width = Size.maxWidth
}
height = width / videoOrientation.aspectRatio
if height > Size.maxHeight {
height = Size.maxHeight
width = height * videoOrientation.aspectRatio
}
return CGSize(width: width, height: height)
}
private func adjustedPosition(for center: CGPoint) -> CGPoint {
guard let superview = containerView.superview else { return center }
var adjustedCenter = center
let halfWidth = containerView.bounds.width / 2
let halfHeight = containerView.bounds.height / 2
let safeAreaInsets: UIEdgeInsets
if #available(iOS 11.0, *) {
safeAreaInsets = superview.safeAreaInsets
} else {
safeAreaInsets = .zero
}
let topLimit = safeAreaInsets.top + halfHeight
let bottomLimit = superview.bounds.height - safeAreaInsets.bottom - halfHeight
adjustedCenter.x = min(max(halfWidth, adjustedCenter.x),
superview.bounds.width - halfWidth)
superview.bounds.width - halfWidth)
adjustedCenter.y = min(max(topLimit, adjustedCenter.y),
bottomLimit)
bottomLimit)
return adjustedCenter
}
private func handlePanEndedWithVelocity(_ velocity: CGPoint) {
let magnitude = sqrt((velocity.x * velocity.x) + (velocity.y * velocity.y))
let slideMultiplier = magnitude / 200
let slideFactor = 0.1 * slideMultiplier
var finalCenter = CGPoint(
x: containerView.center.x + (velocity.x * slideFactor),
y: containerView.center.y + (velocity.y * slideFactor)
)
finalCenter = adjustedPosition(for: finalCenter)
UIView.animate(withDuration: 0.3) {
self.containerView.center = finalCenter
self.delegate?.floatingWindow(self, didChangePosition: finalCenter)
......@@ -399,12 +426,12 @@ class YHFloatingWindow: NSObject {
extension YHFloatingWindow: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch) -> Bool {
shouldReceive touch: UITouch) -> Bool {
let location = touch.location(in: containerView)
if closeButtonContainer.frame.contains(location) {
return false
......
......@@ -104,9 +104,9 @@ class YHMyViewController: YHBaseViewController, ConstraintRelatableTarget {
self.navigationController?.pushViewController(vc)
// let url = "https://pull-flv-l11.douyincdn.com/thirdgame/stream-116296425803875148.flv?expire=1733558990&sign=cc69d0ac884efe3613385140611c1702&major_anchor_level=common&abr_pts=-800&_session_id=037-2024113016095034A5715FA5656D873A69.1732954190959.73911&rsi=1"
// YHPlayerManager.shared.enterLive(from: nil, id: 23, url: url)
// let url = "https://pull-flv-l13.douyincdn.com/stage/stream-116307521507688888_ld5.flv?expire=1733728207&sign=cef0df720ef0dbe3126675d72dcacec2&major_anchor_level=common&abr_pts=-800&_session_id=037-2024120215100750CB84D802B8201D3D81.1733123408338.51633&rsi=1"
// let playbackInfo = YHPlayerManager.PlaybackInfo(id: 40, url: url, isLive: true, scene: .fullscreen)
// YHPlayerManager.shared.enterLive(from: nil, playbackInfo: playbackInfo)
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment