Commit 613ab8a6 authored by Alex朱枝文's avatar Alex朱枝文

直播、录播、banner等播放器管理类

parent 2a71c649
......@@ -302,7 +302,6 @@
04564D532CF38FE2004456E4 /* YHVODPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D522CF38FE2004456E4 /* YHVODPlayerViewController.swift */; };
04564D562CF4467B004456E4 /* YHPlayerTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D552CF4467B004456E4 /* YHPlayerTopBarView.swift */; };
04564D592CF470B2004456E4 /* YHIncomeRecordCompanyTipsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D582CF470B2004456E4 /* YHIncomeRecordCompanyTipsCell.swift */; };
04564D5D2CF49F0A004456E4 /* live_room_test_bg.png in Resources */ = {isa = PBXBuildFile; fileRef = 04564D5C2CF49F0A004456E4 /* live_room_test_bg.png */; };
04564D5F2CF565C7004456E4 /* YHInputBottomBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D5E2CF565C7004456E4 /* YHInputBottomBar.swift */; };
04564D612CF59835004456E4 /* YHMessageInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D602CF59835004456E4 /* YHMessageInputViewController.swift */; };
04564D632CF60222004456E4 /* YHGradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D622CF60222004456E4 /* YHGradientView.swift */; };
......@@ -313,6 +312,10 @@
04564D6E2CF6EB3D004456E4 /* YHHuanXinUserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D6D2CF6EB3D004456E4 /* YHHuanXinUserModel.swift */; };
04564D702CF6EC8A004456E4 /* YHRecordedDetailModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D6F2CF6EC8A004456E4 /* YHRecordedDetailModel.swift */; };
04564D722CF6F18A004456E4 /* YHIMHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D712CF6F18A004456E4 /* YHIMHelper.swift */; };
04564D7A2CF8CEF0004456E4 /* YHPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D792CF8CEF0004456E4 /* YHPlayerManager.swift */; };
04564D7C2CF8CF6D004456E4 /* YHPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D7B2CF8CF6D004456E4 /* YHPlayer.swift */; };
04564D7E2CF8D03D004456E4 /* YHFloatingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D7D2CF8D03D004456E4 /* YHFloatingWindow.swift */; };
04564D802CF8E16C004456E4 /* YHPlayerTransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04564D7F2CF8E16C004456E4 /* YHPlayerTransitionAnimator.swift */; };
0457920B2CBCE7B200EBD99B /* YHResignUploadTravelCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457920A2CBCE7B200EBD99B /* YHResignUploadTravelCardViewModel.swift */; };
0457920D2CBCE8A800EBD99B /* YHResignUploadTravelCardListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457920C2CBCE8A800EBD99B /* YHResignUploadTravelCardListModel.swift */; };
0457920F2CBCE9D000EBD99B /* YHResignUploadTravelCardListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457920E2CBCE9D000EBD99B /* YHResignUploadTravelCardListTableViewCell.swift */; };
......@@ -1367,7 +1370,6 @@
04564D522CF38FE2004456E4 /* YHVODPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHVODPlayerViewController.swift; sourceTree = "<group>"; };
04564D552CF4467B004456E4 /* YHPlayerTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHPlayerTopBarView.swift; sourceTree = "<group>"; };
04564D582CF470B2004456E4 /* YHIncomeRecordCompanyTipsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHIncomeRecordCompanyTipsCell.swift; sourceTree = "<group>"; };
04564D5C2CF49F0A004456E4 /* live_room_test_bg.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = live_room_test_bg.png; sourceTree = "<group>"; };
04564D5E2CF565C7004456E4 /* YHInputBottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHInputBottomBar.swift; sourceTree = "<group>"; };
04564D602CF59835004456E4 /* YHMessageInputViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHMessageInputViewController.swift; sourceTree = "<group>"; };
04564D622CF60222004456E4 /* YHGradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHGradientView.swift; sourceTree = "<group>"; };
......@@ -1378,6 +1380,10 @@
04564D6D2CF6EB3D004456E4 /* YHHuanXinUserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHHuanXinUserModel.swift; sourceTree = "<group>"; };
04564D6F2CF6EC8A004456E4 /* YHRecordedDetailModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHRecordedDetailModel.swift; sourceTree = "<group>"; };
04564D712CF6F18A004456E4 /* YHIMHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHIMHelper.swift; sourceTree = "<group>"; };
04564D792CF8CEF0004456E4 /* YHPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHPlayerManager.swift; sourceTree = "<group>"; };
04564D7B2CF8CF6D004456E4 /* YHPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHPlayer.swift; sourceTree = "<group>"; };
04564D7D2CF8D03D004456E4 /* YHFloatingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHFloatingWindow.swift; sourceTree = "<group>"; };
04564D7F2CF8E16C004456E4 /* YHPlayerTransitionAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHPlayerTransitionAnimator.swift; sourceTree = "<group>"; };
0457920A2CBCE7B200EBD99B /* YHResignUploadTravelCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHResignUploadTravelCardViewModel.swift; sourceTree = "<group>"; };
0457920C2CBCE8A800EBD99B /* YHResignUploadTravelCardListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHResignUploadTravelCardListModel.swift; sourceTree = "<group>"; };
0457920E2CBCE9D000EBD99B /* YHResignUploadTravelCardListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHResignUploadTravelCardListTableViewCell.swift; sourceTree = "<group>"; };
......@@ -2962,11 +2968,14 @@
04564D402CF1BE99004456E4 /* C */ = {
isa = PBXGroup;
children = (
04564D792CF8CEF0004456E4 /* YHPlayerManager.swift */,
04564D482CF385D9004456E4 /* YHBasePlayerViewController.swift */,
04564D602CF59835004456E4 /* YHMessageInputViewController.swift */,
04564D4A2CF389DD004456E4 /* YHLivePlayerViewController.swift */,
04564D6B2CF6C414004456E4 /* YHLivePlayerViewController+Api.swift */,
04564D522CF38FE2004456E4 /* YHVODPlayerViewController.swift */,
04564D7F2CF8E16C004456E4 /* YHPlayerTransitionAnimator.swift */,
04564D7B2CF8CF6D004456E4 /* YHPlayer.swift */,
04564D602CF59835004456E4 /* YHMessageInputViewController.swift */,
);
path = C;
sourceTree = "<group>";
......@@ -2974,7 +2983,6 @@
04564D412CF1BEA2004456E4 /* V */ = {
isa = PBXGroup;
children = (
04564D5C2CF49F0A004456E4 /* live_room_test_bg.png */,
04564D462CF3851D004456E4 /* YHPlayerControlView.swift */,
04564D4C2CF38D16004456E4 /* YHLiveMessageCell.swift */,
04564D4E2CF38E20004456E4 /* YHLiveMessageListView.swift */,
......@@ -2982,6 +2990,7 @@
04564D642CF6065D004456E4 /* YHFadeView.swift */,
04564D552CF4467B004456E4 /* YHPlayerTopBarView.swift */,
04564D5E2CF565C7004456E4 /* YHInputBottomBar.swift */,
04564D7D2CF8D03D004456E4 /* YHFloatingWindow.swift */,
);
path = V;
sourceTree = "<group>";
......@@ -5741,7 +5750,6 @@
A5EE42012C216C78005BBA5D /* img_0.png in Resources */,
04F0ABF32C364F9400518C30 /* home.json in Resources */,
04013E3E2CF87F3A001A8E40 /* zhibo.json in Resources */,
04564D5D2CF49F0A004456E4 /* live_room_test_bg.png in Resources */,
04943BE82CF0A0B500BF2255 /* submit_page_scroll.gif in Resources */,
A5573EDB2B317C0000D98EC0 /* Assets.xcassets in Resources */,
04F0ABF52C364F9400518C30 /* community.json in Resources */,
......@@ -6345,6 +6353,7 @@
0430E65A2C7436CD000511E2 /* YHAdopterNewPeopleViewModel.swift in Sources */,
04256E1D2C75C74200A37BA4 /* YHAppointHKResultModel.swift in Sources */,
04564D592CF470B2004456E4 /* YHIncomeRecordCompanyTipsCell.swift in Sources */,
04564D7C2CF8CF6D004456E4 /* YHPlayer.swift in Sources */,
047F3DE62CE83A0F001B2A6D /* YHHKRequiredItemView.swift in Sources */,
04CE1ADB2C2AD91F001CB80A /* YHActivityTitleItemView.swift in Sources */,
042B20E32CEC92C400655093 /* YHMajorNameCell.swift in Sources */,
......@@ -6407,6 +6416,7 @@
04564D652CF6065D004456E4 /* YHFadeView.swift in Sources */,
04213B272C48C95E00797900 /* YHHomeIdentityCell.swift in Sources */,
041892262C91BDF500B9FB94 /* YHResignDocumentHeaderCell.swift in Sources */,
04564D802CF8E16C004456E4 /* YHPlayerTransitionAnimator.swift in Sources */,
0449EEE92C8EEB1E00A397FD /* YHResinMaterialManageContainerVC.swift in Sources */,
0430E6542C73400A000511E2 /* YHAdopterUploadTableViewCell.swift in Sources */,
04CE1AD32C2AD91F001CB80A /* YHTravelModel.swift in Sources */,
......@@ -6804,6 +6814,7 @@
04F243612C9D488200DF2C74 /* YHHKRecordsPersonnelSelectCell.swift in Sources */,
A5FD63CB2B63D6C300D1D9DA /* YHInformationFillTipsCell.swift in Sources */,
04F9574B2C2032D8003C631C /* YHMyFriendsCell.swift in Sources */,
04564D7A2CF8CEF0004456E4 /* YHPlayerManager.swift in Sources */,
0430E65E2C74624E000511E2 /* YHAdopterCardTableViewCell.swift in Sources */,
045EEF1F2B9F171A0022A143 /* YHDatePickView.swift in Sources */,
04B401E42CE76B10005C61A9 /* YHIncomeInputMoneyCell.swift in Sources */,
......@@ -6812,6 +6823,7 @@
045EEE972B9F171A0022A143 /* YHPreviewInfoWorkExpView.swift in Sources */,
04B401EC2CE84CBD005C61A9 /* YHIncomeTypePopViewSelectCell.swift in Sources */,
04B401F42CEB1C51005C61A9 /* YHIncomeCompanyDetailModel.swift in Sources */,
04564D7E2CF8D03D004456E4 /* YHFloatingWindow.swift in Sources */,
044BACC72BCFA58E00184C64 /* YHNoDataTipsView.swift in Sources */,
04D5C5662B8ED92600190021 /* YHBaseModel.swift in Sources */,
A5F8AC082B9F414000A21EFA /* YHCustomTextView.swift in Sources */,
......@@ -6942,7 +6954,7 @@
);
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = galaxy/Res/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "银河港生活-Test";
INFOPLIST_KEY_CFBundleDisplayName = "银河港生活";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "APP需要您的同意,才能使用相机进行照片拍摄来完成信息填写,如禁止将无法拍摄照片,会影响资料提交效率。";
......@@ -7084,7 +7096,7 @@
);
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = galaxy/Res/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "银河港生活-Uat";
INFOPLIST_KEY_CFBundleDisplayName = "银河港生活";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "APP需要您的同意,才能使用相机进行照片拍摄来完成信息填写,如禁止将无法拍摄照片,会影响资料提交效率。";
......@@ -7289,7 +7301,7 @@
);
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = galaxy/Res/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "银河港生活-Dev";
INFOPLIST_KEY_CFBundleDisplayName = "银河港生活";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "APP需要您的同意,才能使用相机进行照片拍摄来完成信息填写,如禁止将无法拍摄照片,会影响资料提交效率。";
......
......@@ -7,147 +7,164 @@
//
import AgoraRtcKit
import AVKit
import UIKit
class YHBasePlayerViewController: YHBaseViewController {
// MARK: - Properties
var playerKit: AgoraRtcMediaPlayerProtocol!
var agoraKit: AgoraRtcEngineKit!
weak var player: YHPlayer?
var playbackInfo: YHPlayerManager.PlaybackInfo?
// 播放器设置
var currentPlayingURL: String?
var currentVideoTitle: String?
// MARK: - UI Components
lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = .black
return view
}()
lazy var bgIcon: UIImageView = {
let imageView = UIImageView()
if let path = Bundle.main.path(forResource: "live_room_test_bg", ofType: "png") {
imageView.image = UIImage(contentsOfFile: path)
}
return imageView
}()
lazy var playerView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
private lazy var controlView: YHPlayerControlView = {
private(set) lazy var controlView: YHPlayerControlView = {
let view = YHPlayerControlView()
view.delegate = self
return view
}()
lazy var topBarView: YHPlayerTopBarView = {
let view = YHPlayerTopBarView(frame: CGRect.init(x: 0, y: 0, width: KScreenWidth, height: k_Height_NavigationtBarAndStatuBar))
let view = YHPlayerTopBarView(frame: CGRect(x: 0, y: 0, width: KScreenWidth, height: k_Height_NavigationtBarAndStatuBar))
return view
}()
// 控制状态
private var isControlsVisible = true
private var controlsAutoHideTimer: Timer?
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupAgoraKit()
setupGestures()
setupNotifications()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
controlsAutoHideTimer?.invalidate()
controlsAutoHideTimer = nil
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Setup
private func setupUI() {
gk_navBarAlpha = 0
gk_navigationBar.isHidden = true
view.backgroundColor = .black
view.addSubview(containerView)
//containerView.addSubview(bgIcon)
containerView.addSubview(playerView)
containerView.addSubview(controlView)
containerView.addSubview(topBarView)
setupConstraints()
}
private func setupConstraints() {
containerView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
// bgIcon.snp.makeConstraints { make in
// make.edges.equalToSuperview()
// }
playerView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
controlView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
topBarView.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
make.height.equalTo(k_Height_NavigationtBarAndStatuBar)
}
// controlView.snp.makeConstraints { make in
// make.edges.equalToSuperview()
// }
}
private func setupAgoraKit() {
let config = AgoraRtcEngineConfig()
config.appId = YhConstant.AgoraRtcKit.appId
config.areaCode = .global
config.channelProfile = .liveBroadcasting
agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
playerKit = agoraKit.createMediaPlayer(with: self)
playerKit.setView(playerView)
playerKit.setRenderMode(.fit)
private func setupGestures() {
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
containerView.addGestureRecognizer(tap)
}
// MARK: - Controls Visibility
@objc private func handleTap() {
toggleControls()
}
private func toggleControls() {
isControlsVisible.toggle()
controlView.showControls(isControlsVisible)
resetControlsAutoHideTimer()
}
private func resetControlsAutoHideTimer() {
controlsAutoHideTimer?.invalidate()
if isControlsVisible {
controlsAutoHideTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { [weak self] _ in
self?.hideControls()
}
}
}
private func hideControls() {
isControlsVisible = false
controlView.showControls(false)
}
}
extension YHBasePlayerViewController: YHMediaPlayerViewDelegate {
// MARK: - MediaPlayerViewDelegate
func didTapPlayButton() {
if playerKit.getPlayerState() == .playing {
playerKit.pause()
// MARK: - YHPlayerControlViewDelegate
extension YHBasePlayerViewController: YHPlayerControlViewDelegate {
func playerControlView(_ view: YHPlayerControlView, didTapBack button: UIButton) {
if let navigationController = navigationController {
navigationController.popViewController(animated: true)
} else {
playerKit.play()
dismiss(animated: true)
}
}
func didTapStopButton() {
playerKit.stop()
func playerControlView(_ view: YHPlayerControlView, didTapPlay button: UIButton) {
if player?.getPlayState() == .playing {
YHPlayerManager.shared.pause()
} else {
YHPlayerManager.shared.resume()
}
}
func didSeekToPosition(_ position: Float) {
let duration = playerKit.getDuration()
let seekPosition = Int64(Float(duration) * position)
playerKit.seek(toPosition: Int(seekPosition))
func playerControlView(_ view: YHPlayerControlView, didSeekTo position: Float) {
guard let player = player else { return }
let duration = player.playerKit.getDuration()
let targetPosition = Int(Float(duration) * position)
player.playerKit.seek(toPosition: targetPosition)
}
func didChangeQuality(_ quality: YHVideoQuality) {
// 根据不同清晰度切换播放源
switch quality {
case .auto:
// 自动码率逻辑
break
case .sd:
// 切换到标清源
break
case .hd:
// 切换到高清源
break
case .fhd:
// 切换到超清源
break
}
func playerControlView(_ view: YHPlayerControlView, didChangeQuality quality: YHVideoQuality) {
// 由子类实现
}
func didToggleFullscreen() {
// 切换全屏
func playerControlView(_ view: YHPlayerControlView, didTapFullscreen button: UIButton) {
if UIDevice.current.orientation.isPortrait {
let value = UIInterfaceOrientation.landscapeRight.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
......@@ -158,88 +175,91 @@ extension YHBasePlayerViewController: YHMediaPlayerViewDelegate {
}
}
extension YHBasePlayerViewController: AgoraRtcMediaPlayerDelegate {
// MARK: - AgoraRtcMediaPlayerDelegate
func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol,
didChangedTo state: AgoraMediaPlayerState,
reason: AgoraMediaPlayerReason) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
switch state {
case .opening:
print("正在打开媒体源")
case .playing:
self.controlView.updatePlayButton(isPlaying: true)
print("正在播放")
case .paused:
self.controlView.updatePlayButton(isPlaying: false)
print("已暂停")
case .stopped:
self.controlView.updatePlayButton(isPlaying: false)
print("已停止")
case .failed:
print("播放失败,错误原因:\(reason.rawValue)")
self.showAlert(message: "播放失败,错误原因:\(reason.rawValue)")
default:
break
}
}
// MARK: - Notifications
extension YHBasePlayerViewController {
private func setupNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionInterruption),
name: AVAudioSession.interruptionNotification,
object: nil
)
}
func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol,
didChangedTo position: Int) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let duration = self.playerKit.getDuration()
guard duration > 0 else { return }
let progress = Float(position) / Float(duration)
let currentTime = self.formatTime(position)
let totalTime = self.formatTime(duration)
self.controlView.updateProgress(progress,
currentTime: currentTime,
totalTime: totalTime)
}
@objc private func handleAppDidEnterBackground() {
YHPlayerManager.shared.pause()
}
}
extension YHBasePlayerViewController: AgoraRtcEngineDelegate {
// MARK: - AgoraRtcEngineDelegate
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) {
print("Warning: \(warningCode.rawValue)")
@objc private func handleAppWillEnterForeground() {
YHPlayerManager.shared.resume()
}
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
DispatchQueue.main.async { [weak self] in
self?.showAlert(message: "RTC错误:\(errorCode.rawValue)")
@objc private func handleAudioSessionInterruption(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
switch type {
case .began:
YHPlayerManager.shared.pause()
case .ended:
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
YHPlayerManager.shared.resume()
}
}
@unknown default:
break
}
}
}
// MARK: - Helper Methods
extension YHBasePlayerViewController {
// MARK: - Helper Methods
private func formatTime(_ timeInMilliseconds: Int) -> String {
func formatTime(_ timeInMilliseconds: Int) -> String {
let totalSeconds = timeInMilliseconds / 1000
let minutes = totalSeconds / 60
let seconds = totalSeconds % 60
return String(format: "%02d:%02d", minutes, seconds)
}
func showAlert(message: String) {
let alert = UIAlertController(title: "提示",
message: message,
preferredStyle: .alert)
message: message,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
present(alert, animated: true)
}
}
// MARK: - Orientation Support
extension YHBasePlayerViewController {
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .allButUpsideDown
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
}
//
// YHFloatingWindow.swift
// galaxy
//
// Created by alexzzw on 2024/11/29.
// Copyright © 2024 https://www.galaxy-immi.com. All rights reserved.
//
import Foundation
......@@ -12,74 +12,51 @@ import UIKit
class YHLivePlayerViewController: YHBasePlayerViewController {
// MARK: - Properties
private let liveId: Int
private var roomId: String?
private let messageQueue = DispatchQueue(label: "com.livePlayerRoom.messageQueue")
let viewModel = YHLiveSalesViewModel()
private var listMaxWidth: CGFloat {
return KScreenWidth * 248.0 / 375.0
}
// MARK: - UI Components
private lazy var bottomInputBar: YHInputBottomBar = {
let view = YHInputBottomBar()
view.textViewTappedEvent = { [weak self] in
guard let self = self else {
return
}
self.present(self.inputVC, animated: false)
self?.present(self?.inputVC ?? UIViewController(), animated: false)
}
return view
}()
private lazy var inputVC: YHMessageInputViewController = {
let ctl = YHMessageInputViewController()
ctl.closeCallback = {
//
}
ctl.inputCallback = { [weak self] controller, text in
guard let self = self else {
return
}
if !checkLogin() { return }
guard let roomId = self.viewModel.liveDetailModel?.roomId else {
return
}
YHIMHelper.shared.sendMessage(roomID: roomId, sendText: text) { [weak self] message, error in
guard let self = self else {
return
}
guard let message = message else {
if let error = error {
// 被拉黑
if error.code == .moderationFailed {
YHHUD.flash(message: "发送了敏感信息")
} else if error.code == .userPermissionDenied {
YHHUD.flash(message: "您已被拉黑")
} else {
YHHUD.flash(message: "发送失败")
}
}
return
}
controller.updateText("")
controller.closeKeyboard(nil)
self.appendHistoryMessages([message])
}
self?.handleMessageInput(text: text, controller: controller)
}
return ctl
}()
private lazy var messageListView: YHLiveMessageListView = {
let view = YHLiveMessageListView(frame: CGRect.init(x: 0, y: 0, width: listMaxWidth, height: listMaxWidth))
let view = YHLiveMessageListView(frame: CGRect(x: 0, y: 0, width: listMaxWidth, height: listMaxWidth))
return view
}()
let viewModel = YHLiveSalesViewModel()
// MARK: - Initialization
private let liveId: Int
init(id: Int) {
init(id: Int, url: String? = nil, title: String? = nil, roomId: String? = nil) {
self.liveId = id
self.roomId = roomId
super.init(nibName: nil, bundle: nil)
// 隐藏播控UI
controlView.isHidden = true
player?.delegate = self // 设置播放器代理
if let url = url {
play(url: url, title: title)
}
if let roomId = roomId {
setupChatRoom(roomId: roomId)
}
}
required init?(coder: NSCoder) {
......@@ -89,31 +66,25 @@ class YHLivePlayerViewController: YHBasePlayerViewController {
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupLiveUI()
setupLiveNotifications()
setupData()
setupNoti()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
/// 是否可以返回,包括点击返回和手势返回,默认YES
override func navigationShouldPop() -> Bool {
quitChatRoom()
navigationController?.popViewController(animated: true)
return false
}
// MARK: - Setup
private func setupUI() {
private func setupLiveUI() {
containerView.addSubview(bottomInputBar)
containerView.addSubview(messageListView)
bottomInputBar.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-YHInputBottomBar.inputActionHeight)
make.top.equalTo(view.safeAreaLayoutGuide.snp.bottom)
.offset(-YHInputBottomBar.inputActionHeight)
}
messageListView.snp.makeConstraints { make in
......@@ -123,71 +94,121 @@ class YHLivePlayerViewController: YHBasePlayerViewController {
}
topBarView.closeButtonClickEvent = { [weak self] in
guard let self = self else {
return
self?.quitChatRoom()
YHPlayerManager.shared.stop(type: .main)
if let navigationController = self?.navigationController {
navigationController.popViewController(animated: true)
} else {
self?.dismiss(animated: true)
}
self.quitChatRoom()
self.navigationController?.popViewController(animated: true)
}
topBarView.zoomButtonClickEvent = { [weak self] in
self?.enterFloating()
}
}
private func setupData() {
requestData(id: liveId) { [weak self] liveDetail, error in
guard let self = self else {
return
}
guard let liveDetail = liveDetail else {
printLog("YHLivePlayerViewController: 请求失败")
if let errorMsg = error?.errorMsg, errorMsg.count > 0 {
viewModel.getLiveDetail(id: liveId) { [weak self] liveDetail, error in
guard let self = self else { return }
if let liveDetail = liveDetail {
self.handleLiveDetailSuccess(liveDetail)
} else {
printLog("YHLivePlayerViewController: 请求失败")
if let errorMsg = error?.errorMsg, !errorMsg.isEmpty {
YHHUD.flash(message: errorMsg)
}
return
}
self.topBarView.setupTopBarView(headUrl: liveDetail.avatar, nickname: liveDetail.hxNickname, count: liveDetail.access_num)
self.play(with: liveDetail.pullUrl)
YHIMHelper.shared.joinChatRoom(roomID: liveDetail.roomId, leaveOtherRooms: true) { [weak self] error in
guard let self = self else {
return
}
if let error = error {
printLog("joinChatRoom: \(error)")
} else {
printLog("joinChatRoom: success")
YHIMHelper.shared.fetchHistoryMessage(roomID: liveDetail.roomId) { [weak self] list, error in
guard let self = self else {
return
}
guard let list = list else {
return
}
self.messageListView.clearMessages()
self.appendHistoryMessages(list)
}
}
}
}
}
private func setupNoti() {
NotificationCenter.default.addObserver(self, selector: #selector(didChatManagerReceiveMessages(_:)), name: YHIMHelper.didChatManagerReceiveMessages, object: nil)
private func handleLiveDetailSuccess(_ liveDetail: YHLiveDetailModel) {
// 更新顶部栏信息
topBarView.setupTopBarView(
headUrl: liveDetail.avatar,
nickname: liveDetail.hxNickname,
count: liveDetail.access_num
)
NotificationCenter.default.addObserver(self, selector: #selector(didLoginEaseIMSuccess), name: YHIMHelper.didLoginEaseIMSuccess, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didLogOutEaseIM), name: YHIMHelper.didLogOutEaseIM, object: nil)
// 如果没有预设URL,使用接口返回的URL播放
if currentPlayingURL == nil {
play(url: liveDetail.pullUrl)
}
// 如果没有预设roomId,使用接口返回的roomId
if roomId == nil {
roomId = liveDetail.roomId
setupChatRoom(roomId: liveDetail.roomId)
}
}
private func checkLogin()->Bool {
if YHLoginManager.shared.isLogin() == false {
YHOneKeyLoginManager.shared.oneKeyLogin()
return false
}
return true
private func setupChatRoom(roomId: String) {
// 初始化聊天室
joinChatRoom(roomId: roomId)
}
// MARK: - IM Action
private func quitChatRoom() {
guard let roomId = self.viewModel.liveDetailModel?.roomId else {
private func setupLiveNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(didChatManagerReceiveMessages(_:)),
name: YHIMHelper.didChatManagerReceiveMessages,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(didLoginEaseIMSuccess),
name: YHIMHelper.didLoginEaseIMSuccess,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(didLogOutEaseIM),
name: YHIMHelper.didLogOutEaseIM,
object: nil
)
}
// MARK: - Public Methods
func play(url: String, title: String? = nil) {
currentPlayingURL = url
currentVideoTitle = title
controlView.setTitle(title ?? "")
YHPlayerManager.shared.play(url: url, inView: playerView, title: title)
}
func enterFloating() {
guard let playbackInfo = playbackInfo else {
return
}
YHPlayerManager.shared.enterFloating(from: self, playbackInfo: playbackInfo)
}
// MARK: - Chat Room Methods
private func joinChatRoom(roomId: String) {
YHIMHelper.shared.joinChatRoom(roomID: roomId, leaveOtherRooms: true) { [weak self] error in
guard let self = self else { return }
if let error = error {
printLog("joinChatRoom: \(error)")
} else {
self.loadHistoryMessages(roomId: roomId)
}
}
}
private func loadHistoryMessages(roomId: String) {
YHIMHelper.shared.fetchHistoryMessage(roomID: roomId) { [weak self] list, error in
guard let self = self, let messages = list else { return }
self.messageListView.clearMessages()
self.appendHistoryMessages(messages)
}
}
private func quitChatRoom() {
guard let roomId = roomId else { return }
YHIMHelper.shared.quitChatRoom(roomID: roomId) { error in
if let error = error {
......@@ -198,6 +219,31 @@ class YHLivePlayerViewController: YHBasePlayerViewController {
}
}
// MARK: - Message Handling
private func handleMessageInput(text: String, controller: YHMessageInputViewController) {
guard checkLogin(),
let roomId = roomId else { return }
YHIMHelper.shared.sendMessage(roomID: roomId, sendText: text) { [weak self] message, error in
guard let self = self else { return }
if let message = message {
controller.updateText("")
controller.closeKeyboard(nil)
self.appendHistoryMessages([message])
} else if let error = error {
switch error.code {
case .moderationFailed:
YHHUD.flash(message: "发送了敏感信息")
case .userPermissionDenied:
YHHUD.flash(message: "您已被拉黑")
default:
YHHUD.flash(message: "发送失败")
}
}
}
}
private func appendHistoryMessages(_ newMessages: [EMChatMessage]) {
messageQueue.async {
let filterMessages = newMessages.filter { message in
......@@ -211,22 +257,28 @@ class YHLivePlayerViewController: YHBasePlayerViewController {
}
}
}
private func checkLogin() -> Bool {
if !YHLoginManager.shared.isLogin() {
YHOneKeyLoginManager.shared.oneKeyLogin()
return false
}
return true
}
// MARK: - Notification Handlers
@objc private func didChatManagerReceiveMessages(_ note: Notification) {
guard let messages = note.object as? [EMChatMessage], let message = messages.first, message.conversationId == viewModel.liveDetailModel?.roomId else {
guard let messages = note.object as? [EMChatMessage],
let message = messages.first,
message.conversationId == roomId else {
return
}
appendHistoryMessages(messages)
}
@objc private func didLoginEaseIMSuccess() {
guard let roomId = self.viewModel.liveDetailModel?.roomId else {
return
}
YHIMHelper.shared.joinChatRoom(roomID: roomId, leaveOtherRooms: true) { [weak self] error in
guard let self = self else {
return
}
if let roomId = roomId {
joinChatRoom(roomId: roomId)
}
}
......@@ -235,36 +287,43 @@ class YHLivePlayerViewController: YHBasePlayerViewController {
}
}
extension YHLivePlayerViewController {
// MARK: - Player Control
private func play(with url: String) {
let mediaSource = AgoraMediaSource()
mediaSource.url = url
mediaSource.autoPlay = true
let result = playerKit.open(with: mediaSource)
if result != 0 {
showAlert(message: "播放失败,错误码:\(result)")
// MARK: - YHPlayerDelegate
extension YHLivePlayerViewController: YHPlayerDelegate {
func player(_ player: YHPlayer, didChangedToState state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason) {
DispatchQueue.main.async {
// 更新通用UI状态
switch state {
case .playing:
// controlView.updatePlayButton(isPlaying: true)
// 直播开始时的特殊处理
break
case .paused, .stopped:
// controlView.updatePlayButton(isPlaying: false)
// 直播开始时的特殊处理
break
case .failed:
self.showAlert(message: "播放失败,错误原因:\(reason.rawValue)")
default:
break
}
}
}
}
extension YHLivePlayerViewController {
override func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol,
didChangedTo state: AgoraMediaPlayerState,
reason: AgoraMediaPlayerReason) {
super.AgoraRtcMediaPlayer(playerKit, didChangedTo: state, reason: reason)
func player(_ player: YHPlayer, didChangedToPosition position: Int) {
let duration = player.playerKit.getDuration()
guard duration > 0 else { return }
// 添加直播特定的状态处理
switch state {
case .playing:
// 直播开始时的特殊处理
break
case .stopped:
// 直播结束时的特殊处理
break
default:
break
}
let progress = Float(position) / Float(duration)
let currentTime = formatTime(position)
let totalTime = formatTime(duration)
controlView.updateProgress(progress,
currentTime: currentTime,
totalTime: totalTime)
}
func player(_ player: YHPlayer, didReceiveVideoSize size: CGSize) {
// 处理视频尺寸变化,如果需要的话
}
}
//
// YHPlayer.swift
// galaxy
//
// Created by alexzzw on 2024/11/29.
// Copyright © 2024 https://www.galaxy-immi.com. All rights reserved.
//
import AgoraRtcKit
import Foundation
// MARK: - 播放器类型
enum YHPlayerType {
case main
case secondary
}
protocol YHPlayerDelegate: AnyObject {
func player(_ player: YHPlayer, didChangedToState state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason)
func player(_ player: YHPlayer, didChangedToPosition position: Int)
func player(_ player: YHPlayer, didReceiveVideoSize size: CGSize)
}
// MARK: - 播放器实例封装
class YHPlayer {
weak var delegate: YHPlayerDelegate?
let type: YHPlayerType
let playerKit: AgoraRtcMediaPlayerProtocol
private(set) var currentURL: String?
weak var currentPlayView: UIView?
private(set) var currentTitle: String?
init(type: YHPlayerType, playerKit: AgoraRtcMediaPlayerProtocol) {
self.type = type
self.playerKit = playerKit
}
func setPlayView(_ view: UIView?) {
currentPlayView = view
playerKit.setView(view)
}
func play(url: String, title: String? = nil) {
currentURL = url
currentTitle = title
let mediaSource = AgoraMediaSource()
mediaSource.url = url
mediaSource.autoPlay = true
playerKit.open(with: mediaSource)
}
func stop() {
playerKit.stop()
currentPlayView = nil
currentURL = nil
currentTitle = nil
}
func pause() {
playerKit.pause()
}
func resume() {
playerKit.play()
}
func getPosition() -> Int {
return playerKit.getPosition()
}
func getDuration() -> Int {
return playerKit.getDuration()
}
func getPlayState() -> AgoraMediaPlayerState {
return playerKit.getPlayerState()
}
}
//
// YHPlayerManager.swift
// galaxy
//
// Created by alexzzw on 2024/11/29.
// Copyright © 2024 https://www.galaxy-immi.com. All rights reserved.
//
import AgoraRtcKit
import UIKit
// MARK: - 播放器管理中心
class YHPlayerManager: NSObject {
// 播放器状态信息
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) {
self.id = id
self.url = url
self.title = title
self.roomId = roomId
self.isLive = isLive
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id && lhs.isLive == rhs.isLive
}
}
static let shared = YHPlayerManager()
// 核心组件
private var agoraKit: AgoraRtcEngineKit!
private var players: [YHPlayerType: YHPlayer] = [:]
private var currentPlaybackInfo: [YHPlayerType: PlaybackInfo] = [:]
private var floatingWindow: YHFloatingWindow?
// 转场动画相关
private var transitionSourceView: UIView?
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)
}
// 创建第二播放器
if let secondaryPlayerKit = agoraKit.createMediaPlayer(with: self) {
players[.secondary] = YHPlayer(type: .secondary, playerKit: secondaryPlayerKit)
}
}
// MARK: - Public Methods
// 获取播放器实例
func getPlayer(_ type: YHPlayerType) -> YHPlayer? {
return players[type]
}
// 播放控制
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) {
players[type]?.resume()
}
func pause(type: YHPlayerType = .main) {
players[type]?.pause()
}
func stop(type: YHPlayerType = .main) {
players[type]?.stop()
}
func stopAllPlayers() {
players.values.forEach { $0.stop() }
}
// 视图控制
func setPlayView(_ view: UIView?, type: YHPlayerType = .main) {
players[type]?.setPlayView(view)
}
// MARK: - 资源管理
func destroy() {
stopAllPlayers()
players.removeAll()
exitFloating()
AgoraRtcEngineKit.destroy()
}
}
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
}
playerVC.playbackInfo = playbackInfo
present(playerVC, from: sourceView)
}
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
}
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
}
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
}
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
if let url = playbackInfo.url {
player.play(url: url, title: playbackInfo.title)
}
// 获取当前播放视图的截图和位置
if let sourceView = player.currentPlayView,
let sourceSuperview = sourceView.superview {
let sourceFrame = sourceSuperview.convert(sourceView.frame, to: window)
let snapshotView = sourceView.snapshotView(afterScreenUpdates: false) ?? UIView()
snapshotView.frame = sourceFrame
// 创建浮窗
let floatingWindow = YHFloatingWindow()
floatingWindow.playbackInfo = playbackInfo
floatingWindow.delegate = self
floatingWindow.player = player
self.floatingWindow = floatingWindow
// 添加截图视图到窗口
window.addSubview(snapshotView)
// 计算目标位置
let targetFrame = floatingWindow.calculateInitialFrame()
// 执行动画
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]
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)
}
}
// 如果有viewController需要关闭,先关闭再显示浮窗
if let viewController = viewController {
viewController.dismiss(animated: true) {
showFloatingWindow()
}
} else {
// 直接显示浮窗
showFloatingWindow()
}
}
}
// 退出浮窗
func exitFloating() {
floatingWindow?.dismiss()
floatingWindow = nil
}
private func present(_ playerVC: UIViewController, from sourceView: UIView?) {
playerVC.modalPresentationStyle = .fullScreen
playerVC.transitioningDelegate = self
transitionSourceView = sourceView
if let topVC = UIApplication.shared.keyWindow?.rootViewController?.topMostViewController() {
topVC.present(playerVC, animated: true) { [weak self] in
self?.exitFloating()
self?.transitionSourceView = nil
}
}
}
}
// MARK: - AgoraRtcEngineDelegate
extension YHPlayerManager: AgoraRtcEngineDelegate {
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) {
printLog("AgoraRtcEngine Warning: \(warningCode.rawValue)")
}
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
printLog("AgoraRtcEngine Error: \(errorCode.rawValue)")
}
}
// MARK: - AgoraRtcMediaPlayerDelegate
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 }) {
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 }) {
player.delegate?.player(player, didChangedToPosition: position)
}
}
func AgoraRtcMediaPlayer(_ playerKit: AgoraRtcMediaPlayerProtocol,
didReceiveVideoSize size: CGSize) {
if let player = players.values.first(where: { $0.playerKit === playerKit }) {
player.delegate?.player(player, didReceiveVideoSize: size)
// 如果是主播放器且在浮窗模式,更新浮窗尺寸
}
// 更新浮窗视频尺寸
if floatingWindow?.player?.playerKit === playerKit {
floatingWindow?.setVideoSize(size)
}
}
}
// 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
}
if playbackInfo.isLive {
enterLive(from: window.contentView, playbackInfo: playbackInfo, type: type)
} else {
enterVOD(from: window.contentView, playbackInfo: playbackInfo, type: type)
}
}
func floatingWindowDidClose(_ window: YHFloatingWindow) {
// 关闭浮窗时停止播放
stopAllPlayers()
floatingWindow = nil
}
func floatingWindow(_ window: YHFloatingWindow, didChangeSize size: CGSize) {
// 处理浮窗大小变化
}
func floatingWindow(_ window: YHFloatingWindow, didChangePosition point: CGPoint) {
// 处理浮窗位置变化
}
}
// MARK: - UIViewControllerTransitioningDelegate
extension YHPlayerManager: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController,
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)
} else {
return YHPlayerTransitionAnimator(type: .pop)
}
}
}
extension UIViewController {
func topMostViewController() -> UIViewController {
if let presented = presentedViewController {
return presented.topMostViewController()
}
if let navigation = self as? UINavigationController {
return navigation.visibleViewController?.topMostViewController() ?? navigation
}
if let tab = self as? UITabBarController {
return tab.selectedViewController?.topMostViewController() ?? tab
}
return self
}
}
//
// YHPlayerTransitionAnimator.swift
// galaxy
//
// Created by alexzzw on 2024/11/29.
// Copyright © 2024 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
//class YHPlayerTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
// enum TransitionType {
// case zoomIn // 放大进入
// case zoomOut // 缩小退出
// }
//
// private let type: TransitionType
// private let sourceView: UIView?
// private let sourceFrame: CGRect?
//
// init(type: TransitionType, sourceView: UIView? = nil) {
// self.type = type
// self.sourceView = sourceView
//
// // 获取源视图在屏幕上的frame
// if let sourceView = sourceView {
// self.sourceFrame = sourceView.convert(sourceView.bounds, to: nil)
// } else {
// // 默认的浮窗位置和大小
// self.sourceFrame = CGRect(
// x: UIScreen.main.bounds.width - 150,
// y: UIScreen.main.bounds.height - 200,
// width: 140,
// height: 80
// )
// }
//
// super.init()
// }
//
// func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
// return 0.3
// }
//
// func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// guard let toVC = transitionContext.viewController(forKey: .to),
// let fromVC = transitionContext.viewController(forKey: .from) else {
// transitionContext.completeTransition(false)
// return
// }
//
// let containerView = transitionContext.containerView
// let duration = transitionDuration(using: transitionContext)
//
// switch type {
// case .zoomIn:
// // 放大进入动画
// guard let toView = toVC.view else {
// transitionContext.completeTransition(false)
// return
// }
//
// containerView.addSubview(toView)
//
// // 设置初始状态
// if let sourceFrame = sourceFrame {
// toView.frame = sourceFrame
// } else {
// toView.transform = CGAffineTransform(scaleX: 0.3, y: 0.3)
// toView.alpha = 0
// }
//
// // 执行动画
// UIView.animate(withDuration: duration,
// delay: 0,
// options: .curveEaseInOut,
// animations: {
// if self.sourceFrame != nil {
// toView.frame = UIScreen.main.bounds
// } else {
// toView.transform = .identity
// toView.alpha = 1
// }
// }, completion: { finished in
// transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
// })
//
// case .zoomOut:
// // 缩小退出动画
// guard let fromView = fromVC.view else {
// transitionContext.completeTransition(false)
// return
// }
//
// // 执行动画
// UIView.animate(withDuration: duration,
// delay: 0,
// options: .curveEaseInOut,
// animations: {
// if let sourceFrame = self.sourceFrame {
// fromView.frame = sourceFrame
// } else {
// fromView.transform = CGAffineTransform(scaleX: 0.3, y: 0.3)
// fromView.alpha = 0
// }
// }, completion: { finished in
// fromView.removeFromSuperview()
// transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
// })
// }
// }
//}
class YHPlayerTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
enum TransitionType {
case zoomIn // 放大进入
case zoomOut // 缩小退出
case push // 推入
case pop // 推出
}
private let type: TransitionType
private let sourceView: UIView?
private let sourceFrame: CGRect?
init(type: TransitionType, sourceView: UIView? = nil) {
self.type = type
self.sourceView = sourceView
// 获取源视图在屏幕上的frame
if let sourceView = sourceView {
self.sourceFrame = sourceView.convert(sourceView.bounds, to: nil)
} else {
// 默认的浮窗位置和大小
self.sourceFrame = CGRect(
x: UIScreen.main.bounds.width - 150,
y: UIScreen.main.bounds.height - 200,
width: 140,
height: 80
)
}
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toVC = transitionContext.viewController(forKey: .to),
let fromVC = transitionContext.viewController(forKey: .from) else {
transitionContext.completeTransition(false)
return
}
let containerView = transitionContext.containerView
let duration = transitionDuration(using: transitionContext)
let screenBounds = UIScreen.main.bounds
switch type {
case .zoomIn:
animateZoomIn(containerView: containerView,
fromVC: fromVC,
toVC: toVC,
duration: duration,
transitionContext: transitionContext)
case .zoomOut:
animateZoomOut(containerView: containerView,
fromVC: fromVC,
toVC: toVC,
duration: duration,
transitionContext: transitionContext)
case .push:
guard let toView = toVC.view else { return }
containerView.addSubview(toView)
// 设置初始位置(从右边推入)
toView.frame = screenBounds.offsetBy(dx: screenBounds.width, dy: 0)
UIView.animate(withDuration: duration,
delay: 0,
options: .curveEaseInOut,
animations: {
// 移动到目标位置
toView.frame = screenBounds
fromVC.view.frame = screenBounds.offsetBy(dx: -screenBounds.width * 0.3, dy: 0)
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
case .pop:
guard let fromView = fromVC.view,
let toView = toVC.view else { return }
// 确保目标视图在底层
containerView.insertSubview(toView, belowSubview: fromView)
// 设置初始位置
toView.frame = screenBounds.offsetBy(dx: -screenBounds.width * 0.3, dy: 0)
UIView.animate(withDuration: duration,
delay: 0,
options: .curveEaseInOut,
animations: {
// 移动到目标位置
fromView.frame = screenBounds.offsetBy(dx: screenBounds.width, dy: 0)
toView.frame = screenBounds
}, completion: { finished in
fromView.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
private func animateZoomIn(containerView: UIView,
fromVC: UIViewController,
toVC: UIViewController,
duration: TimeInterval,
transitionContext: UIViewControllerContextTransitioning) {
guard let toView = toVC.view else { return }
containerView.addSubview(toView)
if let sourceFrame = sourceFrame {
// 有源视图时的放大动画
toView.frame = sourceFrame
UIView.animate(withDuration: duration,
animations: {
toView.frame = UIScreen.main.bounds
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
} else {
// 无源视图时执行push动画
toView.frame = UIScreen.main.bounds.offsetBy(dx: UIScreen.main.bounds.width, dy: 0)
UIView.animate(withDuration: duration,
animations: {
toView.frame = UIScreen.main.bounds
fromVC.view.frame = UIScreen.main.bounds.offsetBy(dx: -UIScreen.main.bounds.width * 0.3, dy: 0)
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
private func animateZoomOut(containerView: UIView,
fromVC: UIViewController,
toVC: UIViewController,
duration: TimeInterval,
transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = fromVC.view else { return }
if let sourceFrame = sourceFrame {
// 有源视图时的缩小动画
UIView.animate(withDuration: duration,
animations: {
fromView.frame = sourceFrame
}, completion: { finished in
fromView.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
} else {
// 无源视图时执行pop动画
containerView.insertSubview(toVC.view, belowSubview: fromView)
toVC.view.frame = UIScreen.main.bounds.offsetBy(dx: -UIScreen.main.bounds.width * 0.3, dy: 0)
UIView.animate(withDuration: duration,
animations: {
fromView.frame = UIScreen.main.bounds.offsetBy(dx: UIScreen.main.bounds.width, dy: 0)
toVC.view.frame = UIScreen.main.bounds
}, completion: { finished in
fromView.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
}
......@@ -6,168 +6,106 @@
// Copyright © 2024 https://www.galaxy-immi.com. All rights reserved.
//
import AgoraRtcKit
// YHVODPlayerViewController.swift
import UIKit
import AgoraRtcKit
class YHVODPlayerViewController: YHBasePlayerViewController {
private let videoInfo: YHVideoInfo
private var playConfig: YHVideoPlayConfig
// MARK: - Properties
private let vodId: Int
//private let viewModel = YHVideoViewModel()
init(videoInfo: YHVideoInfo, playConfig: YHVideoPlayConfig = YHVideoPlayConfig()) {
self.videoInfo = videoInfo
self.playConfig = playConfig
// MARK: - Initialization
init(id: Int, url: String? = nil, title: String? = nil) {
self.vodId = id
super.init(nibName: nil, bundle: nil)
player?.delegate = self
if let url = url {
//play(url: url, title: title)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupVideoInfo()
startPlayback()
loadVideoDetail()
}
private func setupVideoInfo() {
// 设置视频标题
title = videoInfo.title
// 设置作者信息
setupAuthorInfo()
// 设置播放器配置
setupPlayerConfig()
// MARK: - Data Loading
private func loadVideoDetail() {
// viewModel.getVideoDetail(videoId: vodId) { [weak self] videoDetail, error in
// guard let self = self else { return }
//
// if let videoDetail = videoDetail {
// self.handleVideoDetailSuccess(videoDetail)
// } else {
// printLog("YHVODPlayerViewController: 请求失败")
// if let errorMsg = error?.errorMsg, !errorMsg.isEmpty {
// YHHUD.flash(message: errorMsg)
// }
// }
// }
}
private func setupAuthorInfo() {
// 添加作者信息视图
let authorInfoView = YHVideoAuthorInfoView(author: videoInfo.author)
view.addSubview(authorInfoView)
authorInfoView.snp.makeConstraints { make in
make.top.equalTo(playerView.snp.bottom)
make.left.right.equalToSuperview()
make.height.equalTo(60)
}
}
private func setupPlayerConfig() {
// 设置播放器配置
if playConfig.muteOnStart {
playerKit.adjustPlayoutVolume(0)
}
}
private func startPlayback() {
// 获取合适的视频URL
let networkType: YHNetworkType = getNetworkType()
if let videoUrl = videoInfo.getBestQualityUrl(for: networkType) {
play(with: videoUrl)
} else {
//showAlert(message: "无法获取视频地址")
// private func handleVideoDetailSuccess(_ detail: YHVideoDetailModel) {
// // 如果没有预设URL,使用接口返回的URL播放
// if currentPlayingURL == nil {
// play(url: detail.playUrl, title: detail.title)
// }
// }
}
// MARK: - YHPlayerDelegate
extension YHVODPlayerViewController: YHPlayerDelegate {
func player(_ player: YHPlayer, didChangedToState state: AgoraMediaPlayerState, reason: AgoraMediaPlayerReason) {
switch state {
case .playing:
controlView.updatePlayButton(isPlaying: true)
updatePlayCount()
case .paused, .stopped:
controlView.updatePlayButton(isPlaying: false)
case .failed:
showAlert(message: "播放失败,错误原因:\(reason.rawValue)")
default:
break
}
}
private func getNetworkType() -> YHNetworkType {
// 实现网络类型检测逻辑
return .wifi
}
// MARK: - Player Control
private func play(with url: String) {
let mediaSource = AgoraMediaSource()
mediaSource.url = url
mediaSource.autoPlay = true
func player(_ player: YHPlayer, didChangedToPosition position: Int) {
let duration = player.playerKit.getDuration()
guard duration > 0 else { return }
let result = playerKit.open(with: mediaSource)
if result != 0 {
showAlert(message: "播放失败,错误码:\(result)")
}
let progress = Float(position) / Float(duration)
let currentTime = formatTime(position)
let totalTime = formatTime(duration)
controlView.updateProgress(progress,
currentTime: currentTime,
totalTime: totalTime)
}
private func updatePlayCount() {
// 更新播放次数的逻辑
func player(_ player: YHPlayer, didReceiveVideoSize size: CGSize) {
// 处理视频尺寸变化,如有需要
}
}
// 作者信息视图
class YHVideoAuthorInfoView: UIView {
private let author: YHVideoAuthor
private lazy var avatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.layer.cornerRadius = 20
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
private lazy var nicknameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
return label
}()
private lazy var followButton: UIButton = {
let button = UIButton(type: .system)
button.layer.cornerRadius = 15
button.clipsToBounds = true
return button
}()
init(author: YHVideoAuthor) {
self.author = author
super.init(frame: .zero)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
addSubview(avatarImageView)
addSubview(nicknameLabel)
addSubview(followButton)
// 设置约束
avatarImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
make.size.equalTo(CGSize(width: 40, height: 40))
}
nicknameLabel.snp.makeConstraints { make in
make.left.equalTo(avatarImageView.snp.right).offset(12)
make.centerY.equalToSuperview()
}
followButton.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-16)
make.centerY.equalToSuperview()
make.width.equalTo(80)
make.height.equalTo(30)
}
// 设置数据
nicknameLabel.text = author.nickname
updateFollowButton()
// 加载头像
if let avatarUrl = author.avatarUrl {
// 使用SDWebImage或其他图片加载库加载头像
// avatarImageView.sd_setImage(with: URL(string: avatarUrl))
}
}
private func updateFollowButton() {
let title = author.isFollowed ? "已关注" : "关注"
let backgroundColor = author.isFollowed ? UIColor.systemGray3 : UIColor.systemBlue
followButton.setTitle(title, for: .normal)
followButton.backgroundColor = backgroundColor
followButton.setTitleColor(.white, for: .normal)
// MARK: - Helper Methods
private extension YHVODPlayerViewController {
func updatePlayCount() {
// YHAPIService.shared.updateVideoPlayCount(id: vodId) { [weak self] result in
// switch result {
// case .success:
// break
// case .failure(let error):
// printLog("更新播放次数失败: \(error)")
// }
// }
}
}
......@@ -180,7 +180,7 @@ extension YHIMHelper {
func fetchHistoryMessage(roomID: String, completion: @escaping ([EMChatMessage]?, EMError?) -> Void) {
let option = EMFetchServerMessagesOption()
option.direction = .up // 时间戳逆序的消息因为要倒置表格
EMClient.shared().chatManager?.fetchMessagesFromServer(by: roomID, conversationType: .chatRoom, cursor: nil, pageSize: 50, option: option, completion: { result, err in
EMClient.shared().chatManager?.fetchMessagesFromServer(by: roomID, conversationType: .chatRoom, cursor: nil, pageSize: 10, option: option, completion: { result, err in
DispatchQueue.main.async {
if let err = err {
// 获取失败
......
//
// YHFloatingWindow.swift
// galaxy
//
// Created by alexzzw on 2024/11/29.
// Copyright © 2024 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
// MARK: - 浮窗管理
protocol YHFloatingWindowDelegate: AnyObject {
func floatingWindowDidTap(_ window: YHFloatingWindow)
func floatingWindowDidClose(_ window: YHFloatingWindow)
func floatingWindow(_ window: YHFloatingWindow, didChangeSize size: CGSize)
func floatingWindow(_ window: YHFloatingWindow, didChangePosition point: CGPoint)
}
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
case .landscape: return 16.0 / 9.0
}
}
}
// 窗口尺寸
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 lastScale: CGFloat = 1.0
private var isResizing: Bool = false
private var initialCenter: CGPoint = .zero
// UI组件
private lazy var closeButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "icon_close"), for: .normal)
button.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
return button
}()
// MARK: - Initialization
override init() {
// 创建容器视图
containerView = UIView(frame: CGRect(x: 0, y: 0,
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 = 8
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.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
// 添加关闭按钮
containerView.addSubview(closeButton)
closeButton.snp.makeConstraints { make in
make.top.right.equalToSuperview().inset(8)
make.size.equalTo(CGSize(width: 24, height: 24))
}
}
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 = 150 // 或其他合适的宽度
let height: CGFloat = width * 9 / 16 // 保持16:9比例
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
} else {
// 默认位置:右下角
containerView.frame.origin = CGPoint(
x: window.bounds.width - containerView.bounds.width - 20,
y: window.bounds.height - containerView.bounds.height - 100
)
}
window.addSubview(containerView)
// 显示动画
containerView.alpha = 0
// containerView.transform = CGAffineTransform(scaleX: 0.3, y: 0.3)
UIView.animate(withDuration: 0.3) {
self.containerView.alpha = 1
self.containerView.transform = .identity
}
}
func dismiss() {
UIView.animate(withDuration: 0.3, animations: {
self.containerView.alpha = 0
//self.containerView.transform = CGAffineTransform(scaleX: 0.3, y: 0.3)
}) { _ in
self.containerView.removeFromSuperview()
}
}
func setVideoSize(_ size: CGSize) {
// 更新视频方向
let orientation: VideoOrientation = size.width > size.height ? .landscape : .portrait
if orientation != videoOrientation {
videoOrientation = orientation
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
self.containerView.frame = frame
// 通知代理
self.delegate?.floatingWindow(self, didChangeSize: frame.size)
}
}
private func snapToNearestSize() {
let currentWidth = containerView.bounds.width
// 定义三种尺寸状态
let smallWidth = Size.minWidth
let mediumWidth = Size.maxWidth * 0.5
let largeWidth = Size.maxWidth
// 确定目标尺寸
let targetWidth: CGFloat
if currentWidth < (smallWidth + mediumWidth) / 2 {
targetWidth = smallWidth
} else if currentWidth < (mediumWidth + largeWidth) / 2 {
targetWidth = mediumWidth
} else {
targetWidth = largeWidth
}
// 计算对应高度
let targetHeight = targetWidth / videoOrientation.aspectRatio
// 执行动画
UIView.animate(withDuration: 0.3) {
var frame = self.containerView.frame
frame.size = CGSize(width: targetWidth, height: targetHeight)
frame.origin.x = self.containerView.center.x - targetWidth / 2
frame.origin.y = self.containerView.center.y - 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,
y: initialCenter.y + translation.y
)
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
}
}
@objc private func handlePinch(_ gesture: UIPinchGestureRecognizer) {
switch gesture.state {
case .began:
isResizing = true
initialFrame = containerView.frame
lastScale = 1.0
case .changed:
let scale = gesture.scale / lastScale
let newWidth = initialFrame.width * scale
let newHeight = newWidth / videoOrientation.aspectRatio
var newFrame = initialFrame
newFrame.size = constrainSize(CGSize(width: newWidth, height: newHeight))
newFrame.origin.x = containerView.center.x - newFrame.width / 2
newFrame.origin.y = containerView.center.y - newFrame.height / 2
containerView.frame = newFrame
// 通知代理
delegate?.floatingWindow(self, didChangeSize: newFrame.size)
case .ended:
isResizing = false
snapToNearestSize()
default:
break
}
}
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
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
// 限制最小/最大尺寸
width = max(min(width, Size.maxWidth), Size.minWidth)
height = width / 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
// 限制不超出屏幕边界
adjustedCenter.x = min(max(halfWidth, adjustedCenter.x),
superview.bounds.width - halfWidth)
adjustedCenter.y = min(max(halfHeight, adjustedCenter.y),
superview.bounds.height - halfHeight)
// 计算实际可用区域
let topLimit = superview.safeAreaInsets.top + halfHeight
let bottomLimit = superview.bounds.height - superview.safeAreaInsets.bottom - halfHeight
// 限制不超出屏幕边界和安全区域
adjustedCenter.x = min(max(halfWidth, adjustedCenter.x),
superview.bounds.width - halfWidth)
adjustedCenter.y = min(max(topLimit, adjustedCenter.y),
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)
}
}
}
// MARK: - UIGestureRecognizerDelegate
extension YHFloatingWindow: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch) -> Bool {
// 如果点击的是按钮,不触发点击手势
if touch.view is UIButton {
return false
}
return true
}
}
......@@ -8,20 +8,34 @@
import UIKit
protocol YHPlayerControlViewDelegate: AnyObject {
func playerControlView(_ view: YHPlayerControlView, didTapBack button: UIButton)
func playerControlView(_ view: YHPlayerControlView, didTapPlay button: UIButton)
func playerControlView(_ view: YHPlayerControlView, didSeekTo position: Float)
func playerControlView(_ view: YHPlayerControlView, didChangeQuality quality: YHVideoQuality)
func playerControlView(_ view: YHPlayerControlView, didTapFullscreen button: UIButton)
}
class YHPlayerControlView: UIView {
// MARK: - Properties
weak var delegate: YHMediaPlayerViewDelegate?
weak var delegate: YHPlayerControlViewDelegate?
// MARK: - UI Components
private lazy var dimView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.black.withAlphaComponent(0.4)
return view
}()
private lazy var topBar: UIView = {
let view = UIView()
view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
view.backgroundColor = .clear
return view
}()
private lazy var backButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "chevron.left"), for: .normal)
button.setImage(UIImage(named: "icon_back_white"), for: .normal)
button.tintColor = .white
button.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside)
return button
......@@ -36,24 +50,26 @@ class YHPlayerControlView: UIView {
private lazy var bottomBar: UIView = {
let view = UIView()
view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
view.backgroundColor = .clear
return view
}()
private lazy var playButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "play.fill"), for: .normal)
button.tintColor = .white
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "icon_play"), for: .normal)
button.setImage(UIImage(named: "icon_pause"), for: .selected)
button.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
return button
}()
private lazy var progressSlider: UISlider = {
let slider = UISlider()
slider.minimumTrackTintColor = .systemBlue
slider.maximumTrackTintColor = .gray
slider.setThumbImage(UIImage(systemName: "circle.fill")?.withTintColor(.white, renderingMode: .alwaysOriginal), for: .normal)
slider.minimumTrackTintColor = UIColor.brandMainColor
slider.maximumTrackTintColor = .white.withAlphaComponent(0.3)
slider.setThumbImage(UIImage(named: "icon_slider_thumb"), for: .normal)
slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderTouchBegan(_:)), for: .touchDown)
slider.addTarget(self, action: #selector(sliderTouchEnded(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
return slider
}()
......@@ -68,15 +84,16 @@ class YHPlayerControlView: UIView {
private lazy var qualityButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("清晰度", for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14)
button.setTitleColor(.white, for: .normal)
button.addTarget(self, action: #selector(qualityButtonTapped), for: .touchUpInside)
return button
}()
private lazy var fullscreenButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "arrow.up.left.and.arrow.down.right"), for: .normal)
button.tintColor = .white
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "icon_fullscreen"), for: .normal)
button.setImage(UIImage(named: "icon_fullscreen_exit"), for: .selected)
button.addTarget(self, action: #selector(fullscreenButtonTapped), for: .touchUpInside)
return button
}()
......@@ -94,6 +111,7 @@ class YHPlayerControlView: UIView {
// MARK: - Setup
private func setupUI() {
addSubview(dimView)
addSubview(topBar)
addSubview(bottomBar)
......@@ -110,6 +128,10 @@ class YHPlayerControlView: UIView {
}
private func setupConstraints() {
dimView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
topBar.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
make.height.equalTo(44)
......@@ -122,7 +144,9 @@ class YHPlayerControlView: UIView {
}
titleLabel.snp.makeConstraints { make in
make.center.equalTo(topBar)
make.left.equalTo(backButton.snp.right).offset(8)
make.right.equalTo(topBar).offset(-16)
make.centerY.equalTo(topBar)
}
bottomBar.snp.makeConstraints { make in
......@@ -162,15 +186,25 @@ class YHPlayerControlView: UIView {
// MARK: - Actions
@objc private func backButtonTapped() {
// Handle back action
delegate?.playerControlView(self, didTapBack: backButton)
}
@objc private func playButtonTapped() {
delegate?.didTapPlayButton()
playButton.isSelected.toggle()
delegate?.playerControlView(self, didTapPlay: playButton)
}
@objc private func sliderValueChanged(_ slider: UISlider) {
delegate?.didSeekToPosition(slider.value)
delegate?.playerControlView(self, didSeekTo: slider.value)
timeLabel.text = "\(formatTime(slider.value)) / \(timeLabel.text?.components(separatedBy: " / ").last ?? "00:00")"
}
@objc private func sliderTouchBegan(_ slider: UISlider) {
// 可以在这里暂停播放
}
@objc private func sliderTouchEnded(_ slider: UISlider) {
// 可以在这里恢复播放
}
@objc private func qualityButtonTapped() {
......@@ -178,7 +212,8 @@ class YHPlayerControlView: UIView {
}
@objc private func fullscreenButtonTapped() {
delegate?.didToggleFullscreen()
fullscreenButton.isSelected.toggle()
delegate?.playerControlView(self, didTapFullscreen: fullscreenButton)
}
private func showQualitySelector() {
......@@ -186,7 +221,8 @@ class YHPlayerControlView: UIView {
YHVideoQuality.allCases.forEach { quality in
let action = UIAlertAction(title: quality.rawValue, style: .default) { [weak self] _ in
self?.delegate?.didChangeQuality(quality)
guard let self = self else { return }
self.delegate?.playerControlView(self, didChangeQuality: quality)
}
alert.addAction(action)
}
......@@ -197,13 +233,10 @@ class YHPlayerControlView: UIView {
viewController.present(alert, animated: true)
}
}
}
// MARK: - YHPlayerControlView Extension
extension YHPlayerControlView {
// MARK: - Public Methods
func updatePlayButton(isPlaying: Bool) {
let imageName = isPlaying ? "pause.fill" : "play.fill"
playButton.setImage(UIImage(systemName: imageName), for: .normal)
playButton.isSelected = isPlaying
}
func updateProgress(_ progress: Float, currentTime: String, totalTime: String) {
......@@ -220,4 +253,24 @@ extension YHPlayerControlView {
self.alpha = show ? 1.0 : 0.0
}
}
func updateFullscreenButton(isFullscreen: Bool) {
fullscreenButton.isSelected = isFullscreen
}
// MARK: - Helper Methods
private func formatTime(_ progress: Float) -> String {
let duration = timeLabel.text?.components(separatedBy: " / ").last ?? "00:00"
let components = duration.components(separatedBy: ":")
guard let minuteString = components.first,
let secondString = components.last,
let minutes = Int(minuteString),
let seconds = Int(secondString) else {
return "00:00"
}
let totalSeconds = minutes * 60 + seconds
let currentSeconds = Int(Float(totalSeconds) * progress)
return String(format: "%02d:%02d", currentSeconds / 60, currentSeconds % 60)
}
}
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