From 6f1ad6af19d0fda512b30c184a8d675c2c91b374 Mon Sep 17 00:00:00 2001
From: qinhui <>
Date: Fri, 9 Aug 2024 22:14:34 +0800
Subject: [PATCH] feat: add pip example
---
.../Base.lproj/PictureInPicture.storyboard | 95 -----
.../ChannelViewController.swift | 59 +++
.../CustomViewPIPService.swift | 169 +++++++++
.../CustomViewPIPViewController.swift | 234 ++++++++++++
.../PIPBaseViewController.swift | 12 +
.../PictureInPicture.storyboard | 45 +++
.../PictureInPicture/PictureInPicture.swift | 347 ++----------------
.../PixelBufferPIPService.swift | 114 ++++++
.../PixelBufferPIPViewController.swift | 264 +++++++++++++
.../PixelBufferRenderView.swift | 179 +++++++++
.../zh-Hans.lproj/PictureInPicture.strings | 12 -
iOS/APIExample/Podfile | 2 +
12 files changed, 1114 insertions(+), 418 deletions(-)
delete mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/Base.lproj/PictureInPicture.storyboard
create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/ChannelViewController.swift
create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/CustomViewPIPViewController/CustomViewPIPService.swift
create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/CustomViewPIPViewController/CustomViewPIPViewController.swift
create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PIPBaseViewController.swift
create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PictureInPicture.storyboard
create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferPIPService.swift
create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferPIPViewController.swift
create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferRenderView.swift
delete mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/zh-Hans.lproj/PictureInPicture.strings
diff --git a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/Base.lproj/PictureInPicture.storyboard b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/Base.lproj/PictureInPicture.storyboard
deleted file mode 100644
index 84b72944c..000000000
--- a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/Base.lproj/PictureInPicture.storyboard
+++ /dev/null
@@ -1,95 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/ChannelViewController.swift b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/ChannelViewController.swift
new file mode 100644
index 000000000..49e931b0c
--- /dev/null
+++ b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/ChannelViewController.swift
@@ -0,0 +1,59 @@
+//
+// ViewController.swift
+// PIPDemo
+//
+// Created by qinhui on 2024/8/7.
+//
+
+import UIKit
+
+class ChannelViewController: UIViewController {
+ lazy var textField: UITextField = {
+ let t = UITextField()
+ t.placeholder = "输入房间号"
+ t.borderStyle = .line
+ t.backgroundColor = .orange
+ return t
+ }()
+
+ var pipCls: T.Type?
+
+ lazy var button: UIButton = {
+ let b = UIButton(type: .custom)
+ b.setTitle("加入房间", for: .normal)
+ b.setTitleColor(.blue, for: .normal)
+ b.addTarget(self, action: #selector(joinAction), for: .touchUpInside)
+ return b
+ }()
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .white
+
+ view.addSubview(textField)
+ view.addSubview(button)
+
+ button.snp.makeConstraints { make in
+ make.center.equalTo(view)
+ }
+
+ textField.snp.makeConstraints { make in
+ make.bottom.equalTo(button.snp.top).offset(-50)
+ make.centerX.equalTo(button)
+ make.width.equalTo(150)
+ make.height.equalTo(30)
+ }
+ }
+
+ @objc func joinAction() {
+ guard let channelId = textField.text, let cls = pipCls else { return }
+
+ let vc = cls.init()
+ vc.channelId = channelId
+ self.navigationController?.pushViewController(vc, animated: true)
+ }
+
+ override func touchesBegan(_ touches: Set, with event: UIEvent?) {
+ view.endEditing(true)
+ }
+}
diff --git a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/CustomViewPIPViewController/CustomViewPIPService.swift b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/CustomViewPIPViewController/CustomViewPIPService.swift
new file mode 100644
index 000000000..4e99795c7
--- /dev/null
+++ b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/CustomViewPIPViewController/CustomViewPIPService.swift
@@ -0,0 +1,169 @@
+//
+// RtcManager.swift
+// PIPDemo
+//
+// Created by qinhui on 2024/8/7.
+//
+
+import Foundation
+import AgoraRtcKit
+
+class CustomViewPIPService: NSObject {
+ var rtcEngineDelegate: AgoraRtcEngineDelegate?
+ var videoFrameDelegte: AgoraVideoFrameDelegate?
+
+ weak var localView: UIView?
+ weak var remoteView: UIView?
+ var channelId: String
+
+ private lazy var rtcConfig: AgoraRtcEngineConfig = {
+ let config = AgoraRtcEngineConfig()
+ config.appId = KeyCenter.AppId
+ config.areaCode = .global
+ config.channelProfile = .liveBroadcasting
+ return config
+ }()
+
+ private lazy var rtcEngine: AgoraRtcEngineKit = {
+ let engine = AgoraRtcEngineKit.sharedEngine(with: rtcConfig, delegate: self)
+ engine.setClientRole(.broadcaster)
+ engine.enableAudio()
+ engine.enableVideo()
+ engine.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: CGSize(width: 960, height: 540),
+ frameRate: .fps15,
+ bitrate: AgoraVideoBitrateStandard,
+ orientationMode: .adaptative,
+ mirrorMode: .auto))
+ engine.setVideoFrameDelegate(self)
+ return engine
+ }()
+
+ init(localView: UIView, remoteView: UIView, channelId: String) {
+ self.localView = localView
+ self.remoteView = remoteView
+ self.channelId = channelId
+
+ super.init()
+
+ setupRtcEngin()
+ }
+
+ private func setupRtcEngin() {
+ let videoCanvas = AgoraRtcVideoCanvas()
+ videoCanvas.uid = 0
+ videoCanvas.view = localView
+ videoCanvas.renderMode = .hidden
+
+ rtcEngine.setupLocalVideo(videoCanvas)
+ rtcEngine.startPreview()
+ rtcEngine.setDefaultAudioRouteToSpeakerphone(true)
+ rtcEngine.setVideoFrameDelegate(self)
+
+ let option = AgoraRtcChannelMediaOptions()
+ option.publishCameraTrack = true
+ option.publishMicrophoneTrack = true
+ option.clientRoleType = .broadcaster
+
+ NetworkManager.shared.generateToken(channelName: channelId, success: { [weak self] token in
+ guard let self = self else { return }
+
+ let result = self.rtcEngine.joinChannel(byToken: token, channelId: self.channelId, uid: 0, mediaOptions: option)
+ if result != 0 {
+ ToastView.showWait(text: "joinChannel call failed: \(result), please check your params", view: nil)
+ }
+ })
+ }
+
+ func disable() {
+ rtcEngine.disableAudio()
+ rtcEngine.disableVideo()
+ }
+
+ func leave() {
+ rtcEngine.stopPreview()
+ rtcEngine.leaveChannel(nil)
+ }
+
+}
+
+extension CustomViewPIPService: AgoraRtcEngineDelegate {
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didOccur errorType: AgoraEncryptionErrorType) {
+ rtcEngineDelegate?.rtcEngine?(engine, didOccur: errorType)
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) {
+ rtcEngineDelegate?.rtcEngine?(engine, didJoinChannel: channel, withUid: uid, elapsed: elapsed)
+ }
+
+ /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event
+ /// @param uid uid of remote joined user
+ /// @param elapsed time elapse since current sdk instance join the channel in ms
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
+ // Only one remote video view is available for this
+ // tutorial. Here we check if there exists a surface
+ // view tagged as this uid.
+ let videoCanvas = AgoraRtcVideoCanvas()
+ videoCanvas.uid = uid
+ // the view to be binded
+ videoCanvas.view = remoteView
+ videoCanvas.renderMode = .hidden
+ rtcEngine.setupRemoteVideo(videoCanvas)
+
+ rtcEngineDelegate?.rtcEngine?(engine, didJoinedOfUid: uid, elapsed: elapsed)
+ }
+
+ /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event
+ /// @param uid uid of remote joined user
+ /// @param reason reason why this user left, note this event may be triggered when the remote user
+ /// become an audience in live broadcasting profile
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
+ let videoCanvas = AgoraRtcVideoCanvas()
+ videoCanvas.uid = uid
+ // the view to be binded
+ videoCanvas.view = nil
+ videoCanvas.renderMode = .hidden
+ rtcEngine.setupRemoteVideo(videoCanvas)
+
+ rtcEngineDelegate?.rtcEngine?(engine, didOfflineOfUid: uid, reason: reason)
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, connectionChangedTo state: AgoraConnectionState, reason: AgoraConnectionChangedReason) {
+ rtcEngineDelegate?.rtcEngine?(engine, connectionChangedTo: state, reason: reason)
+ }
+
+ /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel.
+ /// @param stats stats struct
+ func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) {
+ rtcEngineDelegate?.rtcEngine?(engine, reportRtcStats: stats)
+ }
+
+ /// Reports the statistics of the uploading local audio streams once every two seconds.
+ /// @param stats stats struct
+ func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) {
+ rtcEngineDelegate?.rtcEngine?(engine, localAudioStats: stats)
+ }
+
+ /// Reports the statistics of the video stream from each remote user/host.
+ /// @param stats stats struct
+ func rtcEngine(_ engine: AgoraRtcEngineKit, remoteVideoStats stats: AgoraRtcRemoteVideoStats) {
+ rtcEngineDelegate?.rtcEngine?(engine, remoteVideoStats: stats)
+ }
+
+ /// Reports the statistics of the audio stream from each remote user/host.
+ /// @param stats stats struct for current call statistics
+ func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) {
+ rtcEngineDelegate?.rtcEngine?(engine, remoteAudioStats: stats)
+ }
+}
+
+extension CustomViewPIPService: AgoraVideoFrameDelegate {
+ func onCapture(_ videoFrame: AgoraOutputVideoFrame, sourceType: AgoraVideoSourceType) -> Bool {
+ print("")
+ return true
+ }
+
+ func onRenderVideoFrame(_ videoFrame: AgoraOutputVideoFrame, uid: UInt, channelId: String) -> Bool {
+ print("")
+ return true
+ }
+}
diff --git a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/CustomViewPIPViewController/CustomViewPIPViewController.swift b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/CustomViewPIPViewController/CustomViewPIPViewController.swift
new file mode 100644
index 000000000..ebe0ac770
--- /dev/null
+++ b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/CustomViewPIPViewController/CustomViewPIPViewController.swift
@@ -0,0 +1,234 @@
+//
+// VideoViewController.swift
+// PIPDemo
+//
+// Created by qinhui on 2024/8/7.
+//
+
+import UIKit
+import SnapKit
+import AgoraRtcKit
+import AVKit
+
+@available(iOS 15.0, *)
+class CustomViewPIPViewController: PIPBaseViewController {
+ private let containerH = 250.0
+ private var isJoined: Bool = false
+ private var pipController: AVPictureInPictureController?
+ private var videoCallbackController: AVPictureInPictureVideoCallViewController?
+ private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
+
+ private var pipSizes = [
+ CGSize(width: 150, height: 300),
+ CGSize(width: 300, height: 150)
+ ]
+
+ private lazy var pipButton: UIButton = {
+ let button = UIButton(type: .custom)
+ button.setTitle("画中画", for: .normal)
+ button.addTarget(self, action: #selector(pipAction), for: .touchUpInside)
+ button.backgroundColor = .purple
+ return button
+ }()
+
+ private lazy var sizeButton: UIButton = {
+ let button = UIButton(type: .custom)
+ button.setTitle("切换尺寸", for: .normal)
+ button.addTarget(self, action: #selector(sizeAction), for: .touchUpInside)
+ button.backgroundColor = .red
+
+ return button
+ }()
+
+ private lazy var localVideoView: UIView = {
+ let view = UIView()
+ view.backgroundColor = .green
+ return view
+ }()
+
+ private lazy var remoteVideoView: UIView = {
+ let view = UIView()
+ view.backgroundColor = .orange
+ return view
+ }()
+
+ private lazy var videoContainerView: UIView = {
+ let view = UIView()
+ view.backgroundColor = .purple
+ return view
+ }()
+
+ private var rtcService: CustomViewPIPService!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .white
+ initRtc()
+ configViews()
+ if AVPictureInPictureController.isPictureInPictureSupported() {
+ configPIPViewController()
+ }
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ guard let pipController = pipController else { return }
+ pipController.stopPictureInPicture()
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+ rtcService.disable()
+
+ if isJoined {
+ rtcService.leave()
+ }
+ }
+}
+
+@available(iOS 15.0, *)
+extension CustomViewPIPViewController {
+ @objc func pipAction() {
+ guard let pipController = pipController else { return }
+
+ if pipController.isPictureInPictureActive {
+ pipController.stopPictureInPicture()
+ } else {
+ pipController.startPictureInPicture()
+ }
+ }
+
+ @objc func sizeAction() {
+ guard let videoCallbackController = videoCallbackController else { return }
+
+ let i = Int.random(in: 0..
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PictureInPicture.swift b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PictureInPicture.swift
index 01c87c5ea..f686853bb 100644
--- a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PictureInPicture.swift
+++ b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PictureInPicture.swift
@@ -1,334 +1,59 @@
//
-// PictureInPicture.swift
-// APIExample
+// HomeViewController.swift
+// PIPDemo
//
-// Created by 胡润辰 on 2022/4/6.
-// Copyright © 2022 Agora Corp. All rights reserved.
+// Created by qinhui on 2024/8/8.
//
import UIKit
-import AGEVideoLayout
-import AgoraRtcKit
-import MediaPlayer
-class PictureInPictureEntry: UIViewController {
- @IBOutlet weak var joinButton: AGButton!
- @IBOutlet weak var channelTextField: AGTextField!
- let identifier = "PictureInPicture"
-
- override func viewDidLoad() {
- super.viewDidLoad()
- }
-
- @IBAction func doJoinPressed(sender: AGButton) {
- guard let channelName = channelTextField.text else {return}
- // resign channel text field
- channelTextField.resignFirstResponder()
-
- let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil)
- // create new view controller every time to ensure we get a clean vc
- guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {
- return
- }
- newViewController.title = channelName
- newViewController.configs = ["channelName": channelName]
- navigationController?.pushViewController(newViewController, animated: true)
+class Model {
+ var title: String
+ var cls: T.Type
+ init(title: String, cls: T.Type) {
+ self.title = title
+ self.cls = cls
}
}
-@available(iOS 15.0, *)
-class PictureInPictureMain: BaseViewController {
- var localVideo = Bundle.loadVideoView(type: .local, audioOnly: false)
- var remoteVideo = Bundle.loadView(fromNib: "VideoViewSampleBufferDisplayView", withType: SampleBufferDisplayView.self)
- var agoraKit: AgoraRtcEngineKit!
- private lazy var callViewController: AVPictureInPictureVideoCallViewController = {
- let callViewController = AVPictureInPictureVideoCallViewController()
- callViewController.preferredContentSize = view.bounds.size
- callViewController.view.backgroundColor = .clear
- callViewController.modalPresentationStyle = .overFullScreen
- return callViewController
- }()
- var pipController: AVPictureInPictureController?
- var remoteUid: UInt?
- // indicate if current instance has joined channel
- var isJoined: Bool = false
- private lazy var containerView: UIView = {
- let view = UIView()
- view.backgroundColor = .red
- return view
+class PictureInPicture: UITableViewController {
+ lazy var dataArray: [Model] = {
+ if #available(iOS 15.0, *) {
+ return [
+ Model(title: "SDK 渲染", cls: CustomViewPIPViewController.self),
+ Model(title: "多人视频自渲染", cls: PixelBufferPIPViewController.self)
+ ]
+ } else {
+ // Fallback on earlier versions
+ return []
+ }
}()
- // swiftlint: disable function_body_length
override func viewDidLoad() {
super.viewDidLoad()
- // layout render view
- localVideo.setPlaceholder(text: "Local Host".localized)
- remoteVideo.setPlaceholder(text: "Remote Host".localized)
- view.addSubview(containerView)
- containerView.frame = CGRect(x: 0, y: 0, width: SCREENSIZE.width, height: 280)
- containerView.addSubview(localVideo)
- containerView.addSubview(remoteVideo)
- localVideo.translatesAutoresizingMaskIntoConstraints = false
- remoteVideo.translatesAutoresizingMaskIntoConstraints = false
- localVideo.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
- localVideo.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
- localVideo.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
- localVideo.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.5).isActive = true
- remoteVideo.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
- remoteVideo.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
- remoteVideo.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
- remoteVideo.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.5).isActive = true
-
- pipController = AVPictureInPictureController(contentSource: .init(activeVideoCallSourceView: containerView,
- contentViewController: callViewController))
- pipController?.canStartPictureInPictureAutomaticallyFromInline = true
- pipController?.delegate = self
- //iOS 15 workaround
- pipController?.setValue(1, forKey: "controlsStyle")
-
- // set up agora instance when view loadedlet config = AgoraRtcEngineConfig()
- let config = AgoraRtcEngineConfig()
- config.appId = KeyCenter.AppId
- config.channelProfile = .liveBroadcasting
- config.areaCode = GlobalSettings.shared.area
- // setup log file path
- let logConfig = AgoraLogConfig()
- logConfig.level = .info
- config.logConfig = logConfig
- agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
- // Configuring Privatization Parameters
- Util.configPrivatization(agoraKit: agoraKit)
- // get channel name from configs
- guard let channelName = configs["channelName"] as? String,
- let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize,
- let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate,
- let orientation = GlobalSettings.shared.getSetting(key: "orientation")?
- .selectedOption().value as? AgoraVideoOutputOrientationMode else {
- return
- }
- // To enable MPNowPlayingInfoCenter, you need to add the following two private parameters
- agoraKit.setParameters("{\"adm_mix_with_others\":false}")
- agoraKit.setParameters("{\"che.audio.nonmixable.option\":true}")
-
- // make myself a broadcaster
- agoraKit.setChannelProfile(.liveBroadcasting)
- agoraKit.setClientRole(GlobalSettings.shared.getUserRole())
-
- // enable video module and set up video encoding configs
- agoraKit.enableVideo()
- agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution,
- frameRate: fps,
- bitrate: AgoraVideoBitrateStandard,
- orientationMode: orientation,
- mirrorMode: AgoraVideoMirrorMode.auto))
- // set up local video to render your local camera preview
- let videoCanvas = AgoraRtcVideoCanvas()
- videoCanvas.uid = 0
- // the view to be binded
- videoCanvas.view = localVideo.videoView
- videoCanvas.renderMode = .hidden
- agoraKit.setupLocalVideo(videoCanvas)
-
- // Set audio route to speaker
- agoraKit.setDefaultAudioRouteToSpeakerphone(true)
-
- // Setup raw video data frame observer
- agoraKit.setVideoFrameDelegate(self)
-
- // start joining channel
- // 1. Users can only see each other after they join the
- // same channel successfully using the same app id.
- // 2. If app certificate is turned on at dashboard, token is needed
- // when joining channel. The channel name and uid used to calculate
- // the token has to match the ones used for channel join
- let option = AgoraRtcChannelMediaOptions()
- NetworkManager.shared.generateToken(channelName: channelName, success: { token in
- let result = self.agoraKit.joinChannel(byToken: token, channelId: channelName, uid: 0, mediaOptions: option, joinSuccess: nil)
- if result != 0 {
- // Usually happens with invalid parameters
- // Error code description can be found at:
- // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode
- // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code
- self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params")
- }
- })
-
- NotificationCenter.default.addObserver(self,
- selector: #selector(didEnterBackgroundNotification),
- name: UIApplication.willResignActiveNotification,
- object: nil)
+ title = "Picture In Picture"
+ tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
- // swiftlint: enable function_body_length
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- setupPlayintInfoCenter()
- }
-
- private func setupPlayintInfoCenter() {
- UIApplication.shared.beginReceivingRemoteControlEvents()
- var nowPlayingInfo = [String: Any]()
- let path = Bundle.main.path(forResource: "agora-logo", ofType: "png") ?? ""
- guard let image = UIImage(contentsOfFile: path) else { return }
- let artWork = MPMediaItemArtwork(boundsSize: image.size) { _ in
- return image
- }
- nowPlayingInfo[MPMediaItemPropertyArtwork] = artWork
- nowPlayingInfo[MPMediaItemPropertyTitle] = "Song Title"
- nowPlayingInfo[MPMediaItemPropertyArtist] = "Artist Name"
- nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = "Album Name"
- nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = true
- MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
- MPNowPlayingInfoCenter.default().playbackState = .playing
+ override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+ return 50
}
- override var canBecomeFirstResponder: Bool {
- true
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ dataArray.count
}
- deinit {
- NotificationCenter.default.removeObserver(self)
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ let model = dataArray[indexPath.row]
+ let vc = ChannelViewController()
+ vc.pipCls = model.cls
+ self.navigationController?.pushViewController(vc, animated: true)
}
- @objc
- private func didEnterBackgroundNotification() {
- onPIP(_btn: UIButton())
- }
-
- @IBAction func onPIP(_btn: UIButton) {
- if let currentPipController = pipController {
- currentPipController.startPictureInPicture()
- } else {
- showAlert(message: "PIP Support iOS 15+".localized)
- }
- }
-
- override func willMove(toParent parent: UIViewController?) {
- if parent == nil {
- // leave channel when exiting the view
- if isJoined {
- if let pipController = pipController, pipController.isPictureInPictureActive {
- pipController.stopPictureInPicture()
- }
- agoraKit.leaveChannel { (stats) -> Void in
- LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info)
- }
- }
- }
- }
-}
-
-/// agora rtc engine delegate events
-@available(iOS 15.0, *)
-extension PictureInPictureMain: AgoraRtcEngineDelegate {
- /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out
- /// what is happening
- /// Warning code description can be found at:
- /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html
- /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html
- /// @param warningCode warning code of the problem
- func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) {
- LogUtils.log(message: "warning: \(warningCode.description)", level: .warning)
- }
-
- /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand
- /// to let user know something wrong is happening
- /// Error code description can be found at:
- /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode
- /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code
- /// @param errorCode error code of the problem
- func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
- LogUtils.log(message: "error: \(errorCode)", level: .error)
- self.showAlert(title: "Error", message: "Error \(errorCode.description) occur")
- }
-
- /// callback when the local user joins a specified channel.
- /// @param channel
- /// @param uid uid of local user
- /// @param elapsed time elapse since current sdk instance join the channel in ms
- func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) {
- isJoined = true
- LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info)
- }
-
- /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event
- /// @param uid uid of remote joined user
- /// @param elapsed time elapse since current sdk instance join the channel in ms
- func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
- LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info)
-
- remoteVideo.videoView.reset()
- }
-
- /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event
- /// @param uid uid of remote joined user
- /// @param reason reason why this user left, note this event may be triggered when the remote user
- /// become an audience in live broadcasting profile
- func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
- LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info)
-
- // to unlink your view from sdk, so that your view reference will be released
- // note the video will stay at its last frame, to completely remove it
- // you will need to remove the EAGL sublayer from your binded view
-// remoteVideo.videoView.reset()
- }
- func rtcEngine(_ engine: AgoraRtcEngineKit,
- remoteVideoStateChangedOfUid uid: UInt,
- state: AgoraVideoRemoteState, reason: AgoraVideoRemoteReason,
- elapsed: Int) {
- if reason == .remoteMuted {
- let pixelBuffer = MediaUtils.cvPixelBufferRef(from: UIImage(named: "agora-logo") ?? UIImage()).takeRetainedValue()
- let videoFrame = AgoraOutputVideoFrame()
- videoFrame.pixelBuffer = pixelBuffer
- videoFrame.width = Int32(remoteVideo.videoView.frame.width)
- videoFrame.height = Int32(remoteVideo.videoView.frame.height)
- remoteVideo.videoView.renderVideoPixelBuffer(videoFrame)
- }
- }
-}
-
-// MARK: - AgoraVideoDataFrameProtocol
-@available(iOS 15.0, *)
-extension PictureInPictureMain: AgoraVideoFrameDelegate {
- func onCapture(_ videoFrame: AgoraOutputVideoFrame, sourceType: AgoraVideoSourceType) -> Bool {
- true
- }
-
- func onRenderVideoFrame(_ videoFrame: AgoraOutputVideoFrame, uid: UInt, channelId: String) -> Bool {
- remoteVideo.videoView.renderVideoPixelBuffer(videoFrame)
- return true
- }
-
- func getVideoFormatPreference() -> AgoraVideoFormat {
- .cvPixelBGRA
- }
- func getRotationApplied() -> Bool {
- true
- }
-}
-
-@available(iOS 15.0, *)
-extension PictureInPictureMain: AVPictureInPictureControllerDelegate {
- func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
- }
-
- func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
- containerView.removeFromSuperview()
- let vc = pictureInPictureController.contentSource?.activeVideoCallContentViewController
- containerView.frame.size = vc?.view.bounds.size ?? .zero
- vc?.view.addSubview(containerView)
- }
-
- func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController,
- failedToStartPictureInPictureWithError error: Error) {
- }
-
- func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
- containerView.removeFromSuperview()
- containerView.frame.size = CGSize(width: SCREENSIZE.width, height: 280)
- view.addSubview(containerView)
- }
-
- func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
+ let model = dataArray[indexPath.row]
+ cell.textLabel?.text = model.title
+ return cell
}
}
diff --git a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferPIPService.swift b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferPIPService.swift
new file mode 100644
index 000000000..d34047993
--- /dev/null
+++ b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferPIPService.swift
@@ -0,0 +1,114 @@
+//
+// PixelBufferPIPService.swift
+// PIPDemo
+//
+// Created by qinhui on 2024/8/8.
+//
+
+import Foundation
+import AgoraRtcKit
+
+class PixelBufferPIPService: NSObject {
+ var videoFrameDelegte: AgoraVideoFrameDelegate?
+ var rtcEngineDelegate: AgoraRtcEngineDelegate?
+ weak var localView: PixelBufferRenderView?
+
+ var uid: UInt = 0
+ var channelId: String
+
+ private lazy var rtcConfig: AgoraRtcEngineConfig = {
+ let config = AgoraRtcEngineConfig()
+ config.appId = KeyCenter.AppId
+ config.areaCode = .global
+ config.channelProfile = .liveBroadcasting
+ return config
+ }()
+
+ private lazy var rtcEngine: AgoraRtcEngineKit = {
+ let engine = AgoraRtcEngineKit.sharedEngine(with: rtcConfig, delegate: self)
+ engine.setClientRole(.broadcaster)
+ engine.enableAudio()
+ engine.enableVideo()
+ engine.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: CGSize(width: 960, height: 540),
+ frameRate: .fps15,
+ bitrate: AgoraVideoBitrateStandard,
+ orientationMode: .fixedPortrait,
+ mirrorMode: .auto))
+ engine.setVideoFrameDelegate(self)
+
+ return engine
+ }()
+
+ init(channelId: String, uid: UInt, localView: PixelBufferRenderView) {
+ self.channelId = channelId
+ self.uid = uid
+ self.localView = localView
+ super.init()
+
+ setupRtcEngin()
+ }
+
+ private func setupRtcEngin() {
+ let videoCanvas = AgoraRtcVideoCanvas()
+ videoCanvas.uid = 0
+ videoCanvas.view = localView
+ videoCanvas.renderMode = .hidden
+
+ rtcEngine.setupLocalVideo(videoCanvas)
+ rtcEngine.startPreview()
+
+ rtcEngine.setDefaultAudioRouteToSpeakerphone(true)
+ rtcEngine.setVideoFrameDelegate(self)
+
+ let option = AgoraRtcChannelMediaOptions()
+ option.publishCameraTrack = true
+ option.publishMicrophoneTrack = true
+ option.clientRoleType = .broadcaster
+
+ NetworkManager.shared.generateToken(channelName: channelId, success: { [weak self] token in
+ guard let self = self else { return }
+ let result = self.rtcEngine.joinChannel(byToken: token, channelId: self.channelId, uid: self.uid, mediaOptions: option)
+ if result != 0 {
+ ToastView.showWait(text: "joinChannel call failed: \(result), please check your params", view: nil)
+ } else {
+ self.localView?.uid = self.uid
+ }
+ })
+ }
+
+ func disable() {
+ rtcEngine.disableAudio()
+ rtcEngine.disableVideo()
+ }
+
+ func leave() {
+ rtcEngine.stopPreview()
+ rtcEngine.leaveChannel(nil)
+ }
+
+}
+
+extension PixelBufferPIPService: AgoraVideoFrameDelegate {
+ func onCapture(_ videoFrame: AgoraOutputVideoFrame, sourceType: AgoraVideoSourceType) -> Bool {
+ return ((self.videoFrameDelegte?.onCapture?(videoFrame, sourceType: sourceType)) != nil)
+ }
+
+ func onRenderVideoFrame(_ videoFrame: AgoraOutputVideoFrame, uid: UInt, channelId: String) -> Bool {
+ return ((self.videoFrameDelegte?.onRenderVideoFrame?(videoFrame, uid: uid, channelId: channelId)) != nil)
+ }
+}
+
+extension PixelBufferPIPService: AgoraRtcEngineDelegate {
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) {
+ rtcEngineDelegate?.rtcEngine?(engine, didJoinChannel: channel, withUid: uid, elapsed: elapsed)
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
+ rtcEngineDelegate?.rtcEngine?(engine, didJoinedOfUid: uid, elapsed: elapsed)
+ }
+
+ func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
+ rtcEngineDelegate?.rtcEngine?(engine, didOfflineOfUid: uid, reason: reason)
+ }
+
+}
diff --git a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferPIPViewController.swift b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferPIPViewController.swift
new file mode 100644
index 000000000..9378fb7bc
--- /dev/null
+++ b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferPIPViewController.swift
@@ -0,0 +1,264 @@
+//
+// PixelBufferPIPViewController.swift
+// PIPDemo
+//
+// Created by qinhui on 2024/8/8.
+//
+import UIKit
+import AVKit
+import AgoraRtcKit
+
+@available(iOS 15.0, *)
+class PixelBufferPIPViewController: PIPBaseViewController {
+ private let mockUid: UInt = UInt.random(in: 0...100000)
+ private var pipController: AVPictureInPictureController?
+ private var videoCallbackController: AVPictureInPictureVideoCallViewController?
+ var isJoined = false
+ private var pipSizes = [
+ CGSize(width: 150, height: 300),
+ CGSize(width: 300, height: 150)
+ ]
+
+ private lazy var pipButton: UIButton = {
+ let button = UIButton(type: .custom)
+ button.setTitle("画中画", for: .normal)
+ button.setTitleColor(.black, for: .normal)
+ button.addTarget(self, action: #selector(pipAction), for: .touchUpInside)
+
+ return button
+ }()
+
+ private lazy var sizeButton: UIButton = {
+ let button = UIButton(type: .custom)
+ button.setTitle("切换尺寸", for: .normal)
+ button.setTitleColor(.black, for: .normal)
+ button.addTarget(self, action: #selector(sizeAction), for: .touchUpInside)
+
+ return button
+ }()
+
+ private lazy var topLeftView: PixelBufferRenderView = {
+ let view = PixelBufferRenderView()
+ view.backgroundColor = .blue
+ return view
+ }()
+
+ private lazy var topRightView: PixelBufferRenderView = {
+ let view = PixelBufferRenderView()
+ view.backgroundColor = .red
+ return view
+ }()
+
+ private lazy var bottomLeftView: PixelBufferRenderView = {
+ let view = PixelBufferRenderView()
+ view.backgroundColor = .green
+ return view
+ }()
+
+ private lazy var bottomRightView: PixelBufferRenderView = {
+ let view = PixelBufferRenderView()
+ view.backgroundColor = .purple
+ return view
+ }()
+
+ private lazy var videoContainerView: UIView = {
+ let view = UIView()
+ view.backgroundColor = .purple
+ return view
+ }()
+
+ private lazy var displayViews: NSHashTable = {
+ let table = NSHashTable(options: .weakMemory, capacity: 4)
+ table.add(self.topLeftView)
+ table.add(self.topRightView)
+ table.add(bottomLeftView)
+ table.add(bottomRightView)
+ return table
+ }()
+
+ private var rtcService: PixelBufferPIPService!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .white
+ initRtc()
+ configViews()
+ if AVPictureInPictureController.isPictureInPictureSupported() {
+ configPIPViewController()
+ }
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ guard let pipController = pipController else { return }
+ pipController.stopPictureInPicture()
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+ rtcService.disable()
+
+ if isJoined {
+ rtcService.leave()
+ }
+ }
+}
+
+@available(iOS 15.0, *)
+extension PixelBufferPIPViewController {
+ private func configPIPViewController() {
+ let videoCallViewController = AVPictureInPictureVideoCallViewController()
+ videoCallViewController.preferredContentSize = view.bounds.size
+ videoCallViewController.view.backgroundColor = .clear
+ videoCallViewController.modalPresentationStyle = .overFullScreen
+
+ self.videoCallbackController = videoCallViewController
+ pipController = AVPictureInPictureController(contentSource: .init(activeVideoCallSourceView: videoContainerView,
+ contentViewController: videoCallViewController))
+ pipController?.canStartPictureInPictureAutomaticallyFromInline = true
+ pipController?.delegate = self
+ pipController?.setValue(1, forKey: "controlsStyle")
+ }
+
+ private func configViews() {
+ self.view.addSubview(videoContainerView)
+ videoContainerView.addSubview(topLeftView)
+ videoContainerView.addSubview(topRightView)
+ videoContainerView.addSubview(bottomLeftView)
+ videoContainerView.addSubview(bottomRightView)
+
+ self.view.addSubview(pipButton)
+ self.view.addSubview(sizeButton)
+
+ videoContainerView.snp.makeConstraints { make in
+ make.left.top.right.bottom.equalTo(0)
+ }
+
+ topLeftView.snp.makeConstraints { make in
+ make.top.left.equalToSuperview()
+ make.width.equalToSuperview().dividedBy(2)
+ make.height.equalToSuperview().dividedBy(2)
+ }
+
+ topRightView.snp.makeConstraints { make in
+ make.top.equalToSuperview()
+ make.right.equalToSuperview()
+ make.width.equalToSuperview().dividedBy(2)
+ make.height.equalToSuperview().dividedBy(2)
+ }
+
+ bottomLeftView.snp.makeConstraints { make in
+ make.bottom.equalToSuperview()
+ make.left.equalToSuperview()
+ make.width.equalToSuperview().dividedBy(2)
+ make.height.equalToSuperview().dividedBy(2)
+ }
+
+ bottomRightView.snp.makeConstraints { make in
+ make.bottom.equalToSuperview()
+ make.right.equalToSuperview()
+ make.width.equalToSuperview().dividedBy(2)
+ make.height.equalToSuperview().dividedBy(2)
+ }
+
+ pipButton.snp.makeConstraints { make in
+ make.center.equalTo(view)
+ }
+
+ sizeButton.snp.makeConstraints { make in
+ make.top.equalTo(self.pipButton.snp.bottom).offset(10)
+ make.centerX.equalTo(self.pipButton.snp.centerX)
+ }
+ }
+
+ private func initRtc() {
+ guard let channelId = channelId else { return }
+ rtcService = PixelBufferPIPService(channelId: channelId, uid: mockUid, localView: topLeftView)
+ rtcService.videoFrameDelegte = self
+ rtcService.rtcEngineDelegate = self
+ }
+
+ @objc func pipAction() {
+ guard let pipController = pipController else { return }
+
+ if pipController.isPictureInPictureActive {
+ pipController.stopPictureInPicture()
+ } else {
+ pipController.startPictureInPicture()
+ }
+ }
+
+ @objc func sizeAction() {
+ guard let videoCallbackController = videoCallbackController else { return }
+
+ let i = Int.random(in: 0.. Bool {
+ if let view = displayViews.allObjects.first(where: { $0.uid == mockUid }), let pixelBuffer = videoFrame.pixelBuffer {
+ view.renderVideoPixelBuffer(pixelBuffer: pixelBuffer, width: videoFrame.width, height: videoFrame.height)
+ }
+
+ return true
+ }
+
+ func onRenderVideoFrame(_ videoFrame: AgoraOutputVideoFrame, uid: UInt, channelId: String) -> Bool {
+ if let view = displayViews.allObjects.first(where: { $0.uid == uid }) {
+ view.renderFromVideoFrameData(videoData: videoFrame, uid: Int(uid))
+ }
+
+ return true
+ }
+}
diff --git a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferRenderView.swift b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferRenderView.swift
new file mode 100644
index 000000000..330b3f9cb
--- /dev/null
+++ b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PixelBufferPIPViewController/PixelBufferRenderView.swift
@@ -0,0 +1,179 @@
+//
+// PixelBufferRenderView.swift
+// PIPDemo
+//
+// Created by qinhui on 2024/8/8.
+//
+
+import UIKit
+import AVFoundation
+import AgoraRtcKit
+
+enum VideoPosition {
+ case topLeft
+ case topRight
+ case bottomLeft
+ case bottomRight
+}
+
+class PixelBufferRenderView: UIView {
+ var uid: UInt = 0
+ private var videoWidth: Int32 = 0
+ private var videoHeight: Int32 = 0
+
+ lazy var displayLayer: AVSampleBufferDisplayLayer = {
+ let layer = AVSampleBufferDisplayLayer()
+ return layer
+ }()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ configLayers()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func configLayers() {
+ self.layer.addSublayer(displayLayer)
+ displayLayer.frame = self.bounds
+ }
+
+ func createLayer() -> AVSampleBufferDisplayLayer {
+ let layer = AVSampleBufferDisplayLayer()
+ return layer
+ }
+
+ func clean() {
+ uid = 0
+ self.displayLayer.removeFromSuperlayer()
+ self.displayLayer = createLayer()
+ self.layer.addSublayer(displayLayer)
+ }
+
+ func renderFromVideoFrameData(videoData: AgoraOutputVideoFrame, uid: Int) {
+ let width = videoData.width
+ let height = videoData.height
+ let yStride = videoData.yStride
+ let uStride = videoData.uStride
+ let vStride = videoData.vStride
+
+ let yBuffer = videoData.yBuffer
+ let uBuffer = videoData.uBuffer
+ let vBuffer = videoData.vBuffer
+
+ autoreleasepool {
+ var pixelBuffer: CVPixelBuffer?
+ let pixelAttributes: [String: Any] = [kCVPixelBufferIOSurfacePropertiesKey as String: [:]]
+
+ let result = CVPixelBufferCreate(kCFAllocatorDefault,
+ Int(width),
+ Int(height),
+ kCVPixelFormatType_420YpCbCr8Planar,
+ pixelAttributes as CFDictionary,
+ &pixelBuffer)
+
+ guard result == kCVReturnSuccess, let pixelBuffer = pixelBuffer else {
+ print("Unable to create CVPixelBuffer: \(result)")
+ return
+ }
+
+ CVPixelBufferLockBaseAddress(pixelBuffer, .init(rawValue: 0))
+ let yPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)
+ let pixelBufferYBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
+
+ for i in 0.. 0, videoHeight > 0, !CGSizeEqualToSize(self.frame.size, CGSize.zero) else {
+ return
+ }
+
+ let viewWidth = self.frame.size.width
+ let viewHeight = self.frame.size.height
+
+ let videoRatio = CGFloat(videoWidth) / CGFloat(videoHeight)
+ let viewRatio = viewWidth / viewHeight
+
+ var videoSize = CGSize.zero
+ if videoRatio >= viewRatio {
+ videoSize.height = viewHeight
+ videoSize.width = videoSize.height * videoRatio
+ } else {
+ videoSize.width = viewWidth
+ videoSize.height = videoSize.width / videoRatio
+ }
+
+ let xOffset = max(0, (viewWidth - videoSize.width) / 2)
+ let yOffset = max(0, (viewHeight - videoSize.height) / 2)
+ let renderRect = CGRect(x: xOffset, y: yOffset, width: videoSize.width, height: videoSize.height)
+
+ if !renderRect.equalTo(displayLayer.frame) {
+ displayLayer.frame = renderRect
+ }
+ }
+
+}
diff --git a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/zh-Hans.lproj/PictureInPicture.strings b/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/zh-Hans.lproj/PictureInPicture.strings
deleted file mode 100644
index c50d8f1d5..000000000
--- a/iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/zh-Hans.lproj/PictureInPicture.strings
+++ /dev/null
@@ -1,12 +0,0 @@
-
-/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "AmK-zc-ByT"; */
-"AmK-zc-ByT.title" = "加入频道";
-
-/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */
-"GWc-L5-fZV.placeholder" = "输入频道名";
-
-/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "cAG-6V-STC"; */
-"cAG-6V-STC.title" = "画中画";
-
-/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */
-"kbN-ZR-nNn.normalTitle" = "加入频道";
diff --git a/iOS/APIExample/Podfile b/iOS/APIExample/Podfile
index 1a98a1409..4e2cea8d0 100644
--- a/iOS/APIExample/Podfile
+++ b/iOS/APIExample/Podfile
@@ -13,6 +13,8 @@ target 'APIExample' do
# pod 'MobileVLCKit', '3.5.1'
pod 'SwiftLint', '~> 0.53.0'
pod 'AgoraRtcEngine_iOS', '4.4.0'
+ pod 'SnapKit', '~> 5.7.0'
+
# pod 'sdk', :path => 'sdk.podspec'
# pod 'senseLib', :path => 'sense.podspec'
# pod 'bytedEffect', :path => 'bytedEffect.podspec'