Skip to content

Commit a2c717e

Browse files
committed
fix: multi push/render bug fixed
1 parent 14398d2 commit a2c717e

5 files changed

Lines changed: 208 additions & 236 deletions

File tree

iOS/APIExample/APIExample/Common/ExternalVideo/AgoraSampleBufferRender.m

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,13 @@ - (void)layoutDisplayLayer {
6464
CGSize videoSize;
6565
if (videoRatio >= viewRatio) {
6666
videoSize.height = viewHeight;
67-
videoSize.width = videoSize.height * videoRatio;
67+
videoSize.width = viewHeight * videoRatio;
6868
}else {
6969
videoSize.width = viewWidth;
70-
videoSize.height = videoSize.width / videoRatio;
70+
videoSize.height = viewWidth / videoRatio;
7171
}
7272

73-
CGRect renderRect = CGRectMake(0.5 * (viewWidth - videoSize.width), 0.5 * (viewHeight - videoSize.height), videoSize.width, videoSize.height);
74-
73+
CGRect renderRect = CGRectMake(0, 0, viewWidth, viewHeight);
7574
if (!CGRectEqualToRect(renderRect, self.displayLayer.frame)) {
7675
self.displayLayer.frame = renderRect;
7776
}

iOS/APIExample/APIExample/Common/Utils/KFMP4Demuxer.m

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ @interface KFMP4Demuxer () {
5252
@property (nonatomic, assign, readwrite) BOOL audioEOF; // Indicates whether audio has ended.
5353
@property (nonatomic, assign, readwrite) BOOL videoEOF; // Indicates whether video has ended.
5454
@property (nonatomic, assign, readwrite) CGAffineTransform preferredTransform; // Transformation information for the image, e.g., video image rotation.
55+
@property (nonatomic, assign) CGFloat frameRate; // Frame rate of the video.
5556
@end
5657

5758
@implementation KFMP4Demuxer
@@ -233,6 +234,9 @@ - (void)fetchAndSaveDemuxedData {
233234
[self saveSampleBuffer:videoBuffer];
234235
CFRelease(videoBuffer);
235236
}
237+
238+
NSTimeInterval deley = 1000 / self.frameRate;
239+
usleep(deley * 1000);
236240
}
237241
if (self.demuxerStatus == KFMP4DemuxerStatusCompleted) {
238242
NSLog(@"KFMP4Demuxer complete");
@@ -391,6 +395,9 @@ - (void)_setupDemuxReader:(NSError**)error {
391395
AVAssetTrack *videoTrack = [[self.config.asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
392396
_hasVideoTrack = videoTrack ? YES : NO;
393397
if (_hasVideoTrack) {
398+
// Get frame rate
399+
self.frameRate = videoTrack.nominalFrameRate > 0 ? videoTrack.nominalFrameRate : 30;
400+
394401
// Get the image transformation information.
395402
_preferredTransform = videoTrack.preferredTransform;
396403

iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/CustomVideoSourcePushMulti.swift

Lines changed: 100 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,27 @@ class UserModel {
1414
var uid: UInt = 0
1515
var canvasView: SampleBufferDisplayView?
1616
var trackId: UInt32 = 0
17-
var isJoin: Bool = false
18-
var customSource: AgoraYUVImageSourcePush?
17+
var isJoin: Bool = false {
18+
didSet {
19+
self.canvasView?.videoView.isHidden = !isJoin
20+
self.canvasView?.videoView.reset()
21+
}
22+
}
1923
var isEncode: Bool = false
24+
var customSource: AgoraYUVImageSourcePush?
2025
var customEncodeSource: KFMP4Demuxer?
26+
27+
func reset() {
28+
self.customSource?.stopSource()
29+
self.customSource = nil
30+
self.customEncodeSource?.cancelReading()
31+
self.customEncodeSource = nil
32+
33+
self.uid = UInt(Int.random(in: 10000...99999))
34+
self.trackId = 0
35+
self.isJoin = false
36+
self.isEncode = false
37+
}
2138
}
2239

2340
class CustomVideoSourcePushMultiEntry: UIViewController {
@@ -46,16 +63,14 @@ class CustomVideoSourcePushMultiEntry: UIViewController {
4663
}
4764

4865
class CustomVideoSourcePushMultiMain: BaseViewController {
49-
var localVideo = Bundle.loadView(fromNib: "VideoViewSampleBufferDisplayView",
50-
withType: SampleBufferDisplayView.self)
51-
lazy var remoteVideos: [UserModel] = (0..<3).map({ _ in
66+
private let localUid: UInt = UInt(Int.random(in: 10000...99999))
67+
lazy var remoteVideos: [UserModel] = (0..<4).map({ _ in
5268
let model = UserModel()
53-
model.uid = UInt(Int.random(in: 10000...99999))
5469
model.canvasView = Bundle.loadView(fromNib: "VideoViewSampleBufferDisplayView",
5570
withType: SampleBufferDisplayView.self)
71+
model.reset()
5672
return model
5773
})
58-
var customCamera: AgoraYUVImageSourcePush?
5974

6075
@IBOutlet weak var container: AGEVideoContainer!
6176
var agoraKit: AgoraRtcEngineKit!
@@ -77,26 +92,14 @@ class CustomVideoSourcePushMultiMain: BaseViewController {
7792
Util.configPrivatization(agoraKit: agoraKit)
7893
agoraKit.setLogFile(LogUtils.sdkLogPath())
7994

80-
container.layoutStream2x2(views: [localVideo] + remoteVideos.compactMap({ $0.canvasView }))
95+
container.layoutStream2x2(views: remoteVideos.compactMap({ $0.canvasView }))
8196
// make myself a broadcaster
8297
// agoraKit.setClientRole(.broadcaster)
8398

8499
// enable video module and set up video encoding configs
85100
agoraKit.enableVideo()
86101
agoraKit.enableAudio()
87102

88-
// setup my own camera as custom video source
89-
// note setupLocalVideo is not working when using pushExternalVideoFrame
90-
// so you will have to prepare the preview yourself
91-
customCamera = AgoraYUVImageSourcePush(size: CGSize(width: 320, height: 180),
92-
fileName: "sample",
93-
frameRate: 15,
94-
isHDR: false)
95-
customCamera?.trackId = agoraKit.createCustomVideoTrack()
96-
customCamera?.delegate = self
97-
customCamera?.startSource()
98-
agoraKit.setExternalVideoSource(true, useTexture: true, sourceType: .videoFrame)
99-
100103
let resolution = (GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize) ?? .zero
101104
let fps = (GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate) ?? .fps15
102105
let orientation = (GlobalSettings.shared.getSetting(key: "orientation")?
@@ -116,29 +119,39 @@ class CustomVideoSourcePushMultiMain: BaseViewController {
116119
// 2. If app certificate is turned on at dashboard, token is needed
117120
// when joining channel. The channel name and uid used to calculate
118121
// the token has to match the ones used for channel join
119-
joinChannel(uid: 999,
120-
trackId: customCamera?.trackId ?? 0,
122+
joinChannel(uid: localUid,
123+
trackId: nil,
121124
publishEncodedVideoTrack: false)
122125
}
123-
private func joinChannel(uid: UInt, trackId: UInt32, publishEncodedVideoTrack: Bool) {
126+
private func joinChannel(uid: UInt, trackId: UInt32?, publishEncodedVideoTrack: Bool) {
124127
guard let channelName = configs["channelName"] as? String else {return}
125128
let option = AgoraRtcChannelMediaOptions()
126-
option.publishCustomVideoTrack = !publishEncodedVideoTrack
127129
option.publishMicrophoneTrack = false
128-
option.autoSubscribeAudio = true
129-
option.autoSubscribeVideo = true
130-
option.customVideoTrackId = Int(trackId)
131-
option.publishEncodedVideoTrack = publishEncodedVideoTrack
132-
option.clientRoleType = GlobalSettings.shared.getUserRole()
133-
let connection = AgoraRtcConnection()
134-
connection.localUid = uid
135-
connection.channelId = channelName
130+
option.publishCameraTrack = false
131+
132+
let delegate: AgoraRtcEngineDelegate? = self
133+
if let trackId = trackId {
134+
option.publishCustomVideoTrack = !publishEncodedVideoTrack
135+
option.customVideoTrackId = Int(trackId)
136+
option.publishEncodedVideoTrack = publishEncodedVideoTrack
137+
option.clientRoleType = .broadcaster
138+
option.autoSubscribeAudio = false
139+
option.autoSubscribeVideo = false
140+
// delegate = nil
141+
} else {
142+
option.clientRoleType = .audience
143+
option.autoSubscribeAudio = true
144+
option.autoSubscribeVideo = true
145+
}
136146
NetworkManager.shared.generateToken(channelName: channelName, uid: uid) { token in
147+
let connection = AgoraRtcConnection()
148+
connection.localUid = uid
149+
connection.channelId = channelName
137150
let result = self.agoraKit.joinChannelEx(byToken: token,
138-
connection: connection,
139-
delegate: self,
140-
mediaOptions: option,
141-
joinSuccess: nil)
151+
connection: connection,
152+
delegate: delegate,
153+
mediaOptions: option,
154+
joinSuccess: nil)
142155
if result != 0 {
143156
// Usually happens with invalid parameters
144157
// Error code description can be found at:
@@ -211,51 +224,61 @@ class CustomVideoSourcePushMultiMain: BaseViewController {
211224

212225
@IBAction func onDestoryVideoTrack(_ sender: Any) {
213226
guard let channelName = configs["channelName"] as? String else {return}
214-
let userModel = remoteVideos.filter({ $0.isJoin == true }).last
215-
userModel?.isJoin = false
216-
userModel?.customSource?.stopSource()
217-
userModel?.customEncodeSource?.cancelReading()
218-
userModel?.customEncodeSource = nil
219-
userModel?.canvasView?.videoView.reset()
220-
userModel?.customSource = nil
227+
guard let userModel = remoteVideos.filter({ $0.trackId != 0 }).last else { return }
228+
let trackId = UInt(userModel.trackId)
221229
let connection = AgoraRtcConnection()
222-
connection.localUid = userModel?.uid ?? 0
230+
connection.localUid = userModel.uid
223231
connection.channelId = channelName
224-
agoraKit.destroyCustomVideoTrack(UInt(userModel?.trackId ?? 0))
225-
agoraKit.destroyCustomEncodedVideoTrack(UInt(userModel?.trackId ?? 0))
226-
userModel?.trackId = 0
227-
userModel?.isEncode = false
228-
userModel?.uid = UInt(Int.random(in: 10000...99999))
232+
agoraKit.destroyCustomVideoTrack(trackId)
233+
agoraKit.destroyCustomEncodedVideoTrack(trackId)
229234
agoraKit.leaveChannelEx(connection) { state in
230-
LogUtils.log(message: "warning: \(state.description)", level: .info)
235+
LogUtils.log(message: "leaveChannelEx: \(state.description)", level: .info)
231236
}
237+
238+
cleanCanvas(uid: userModel.uid)
232239
}
233240

234241
override func willMove(toParent parent: UIViewController?) {
235242
if parent == nil {
236-
// stop capture
237-
customCamera?.stopSource()
238243
// leave channel when exiting the view
239244
if isJoined, let channelName = configs["channelName"] as? String {
240245
remoteVideos.forEach({
241246
let connection = AgoraRtcConnection()
242247
connection.localUid = $0.uid
243248
connection.channelId = channelName
244-
$0.customSource?.stopSource()
245249
agoraKit.leaveChannelEx(connection) { state in
246-
LogUtils.log(message: "warning: \(state.description)", level: .info)
250+
LogUtils.log(message: "leaveChannelEx: \(state.description)", level: .info)
247251
}
252+
$0.reset()
248253
})
249254
let connection = AgoraRtcConnection()
250-
connection.localUid = 999
255+
connection.localUid = localUid
251256
connection.channelId = channelName
252257
agoraKit.leaveChannelEx(connection) { state in
253-
LogUtils.log(message: "warning: \(state.description)", level: .info)
258+
LogUtils.log(message: "leaveChannelEx: \(state.description)", level: .info)
254259
}
255-
AgoraRtcEngineKit.destroy()
256260
}
261+
AgoraRtcEngineKit.destroy()
257262
}
258263
}
264+
265+
private func cleanCanvas(uid: UInt) {
266+
guard let channelName = configs["channelName"] as? String,
267+
let userModel = remoteVideos.first(where: { $0.uid == uid }) else { return }
268+
LogUtils.log(message: "cleanCanvas: \(uid)", level: .info)
269+
270+
// to unlink your view from sdk, so that your view reference will be released
271+
// note the video will stay at its last frame, to completely remove it
272+
// you will need to remove the EAGL sublayer from your binded view
273+
userModel.reset()
274+
275+
let videoCanvas = AgoraRtcVideoCanvas()
276+
videoCanvas.uid = uid
277+
let connect = AgoraRtcConnection()
278+
connect.localUid = localUid
279+
connect.channelId = channelName
280+
agoraKit.setupRemoteVideoEx(videoCanvas, connection: connect)
281+
}
259282
}
260283

261284
/// agora rtc engine delegate events
@@ -267,7 +290,7 @@ extension CustomVideoSourcePushMultiMain: AgoraRtcEngineDelegate {
267290
/// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html
268291
/// @param warningCode warning code of the problem
269292
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) {
270-
LogUtils.log(message: "warning: \(warningCode.description)", level: .warning)
293+
LogUtils.log(message: "didOccurWarning: \(warningCode.description)", level: .warning)
271294
}
272295

273296
/// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand
@@ -277,8 +300,8 @@ extension CustomVideoSourcePushMultiMain: AgoraRtcEngineDelegate {
277300
/// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code
278301
/// @param errorCode error code of the problem
279302
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
280-
LogUtils.log(message: "error: \(errorCode)", level: .error)
281-
self.showAlert(title: "Error", message: "Error \(errorCode.description) occur")
303+
LogUtils.log(message: "didOccurError: \(errorCode)", level: .error)
304+
self.showAlert(title: "Error", message: "didOccurError \(errorCode.description) occur")
282305
}
283306

284307
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) {
@@ -290,58 +313,42 @@ extension CustomVideoSourcePushMultiMain: AgoraRtcEngineDelegate {
290313
/// @param uid uid of remote joined user
291314
/// @param elapsed time elapse since current sdk instance join the channel in ms
292315
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
316+
guard let channelName = configs["channelName"] as? String else {return}
317+
if let _ = remoteVideos.first(where: { $0.uid == uid }) { return }
318+
guard let userModel = remoteVideos.first(where: { $0.isJoin == false && $0.trackId == 0 }) else { return }
293319
LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info)
320+
userModel.uid = uid
321+
userModel.isJoin = true
294322

295-
// Only one remote video view is available for this
296-
// tutorial. Here we check if there exists a surface
297-
// view tagged as this uid.
298-
if uid == 999 { return }
299-
for model in remoteVideos where model.uid == uid {
300-
return
301-
}
323+
// the view to be binded
302324
let videoCanvas = AgoraRtcVideoCanvas()
303325
videoCanvas.uid = uid
304-
// the view to be binded
305-
guard let userModel = remoteVideos.first(where: { $0.isJoin == false }) else { return }
306326
videoCanvas.view = userModel.canvasView?.videoView
307-
videoCanvas.renderMode = .hidden
308-
userModel.uid = uid
309-
userModel.isJoin = true
310-
agoraKit.setupRemoteVideo(videoCanvas)
327+
videoCanvas.renderMode = .fit
328+
329+
let connect = AgoraRtcConnection()
330+
connect.localUid = localUid
331+
connect.channelId = channelName
332+
agoraKit.setupRemoteVideoEx(videoCanvas, connection: connect)
311333
}
312334

313335
/// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event
314336
/// @param uid uid of remote joined user
315337
/// @param reason reason why this user left, note this event may be triggered when the remote user
316338
/// become an audience in live broadcasting profile
317339
func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
340+
guard let _ = remoteVideos.first(where: { $0.uid == uid }) else { return }
318341
LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info)
319342

320-
// to unlink your view from sdk, so that your view reference will be released
321-
// note the video will stay at its last frame, to completely remove it
322-
// you will need to remove the EAGL sublayer from your binded view
323-
let userModel = remoteVideos.first(where: { $0.uid == uid })
324-
userModel?.isJoin = false
325-
userModel?.uid = UInt(Int.random(in: 10000...99999))
326-
userModel?.canvasView?.videoView.reset()
343+
cleanCanvas(uid: uid)
327344
}
328345
}
329346

330347
/// agora camera video source, the delegate will get frame data from camera
331348
extension CustomVideoSourcePushMultiMain: AgoraYUVImageSourcePushDelegate {
332349
func onVideoFrame(_ buffer: CVPixelBuffer, size: CGSize, trackId: UInt, rotation: Int32) {
333350
let videoFrame = AgoraVideoFrame()
334-
/** Video format:
335-
* - 1: I420
336-
* - 2: BGRA
337-
* - 3: NV21
338-
* - 4: RGBA
339-
* - 5: IMC2
340-
* - 7: ARGB
341-
* - 8: NV12
342-
* - 12: iOS texture (CVPixelBufferRef)
343-
*/
344-
videoFrame.format = 12
351+
videoFrame.format = AgoraVideoFormat.cvPixelNV12.rawValue
345352
videoFrame.textureBuf = buffer
346353
videoFrame.rotation = Int32(rotation)
347354

@@ -350,12 +357,8 @@ extension CustomVideoSourcePushMultiMain: AgoraYUVImageSourcePushDelegate {
350357
outputVideoFrame.height = Int32(size.height)
351358
outputVideoFrame.pixelBuffer = buffer
352359
outputVideoFrame.rotation = rotation
353-
if customCamera?.trackId ?? 0 == trackId {
354-
localVideo.videoView.renderVideoPixelBuffer(outputVideoFrame)
355-
} else {
356-
let userModel = remoteVideos.first(where: { $0.trackId == trackId })
357-
userModel?.canvasView?.videoView.renderVideoPixelBuffer(outputVideoFrame)
358-
}
360+
let userModel = remoteVideos.first(where: { $0.trackId == trackId })
361+
userModel?.canvasView?.videoView.renderVideoPixelBuffer(outputVideoFrame)
359362
// once we have the video frame, we can push to agora sdk
360363
agoraKit?.pushExternalVideoFrame(videoFrame, videoTrackId: trackId)
361364
}

macOS/APIExample/Common/Utils/KFMP4Demuxer.m

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ @interface KFMP4Demuxer () {
5252
@property (nonatomic, assign, readwrite) BOOL audioEOF; // Indicates whether audio has ended.
5353
@property (nonatomic, assign, readwrite) BOOL videoEOF; // Indicates whether video has ended.
5454
@property (nonatomic, assign, readwrite) CGAffineTransform preferredTransform; // Transformation information for the image, e.g., video image rotation.
55+
@property (nonatomic, assign) CGFloat frameRate; // Frame rate of the video.
5556
@end
5657

5758
@implementation KFMP4Demuxer
@@ -233,6 +234,9 @@ - (void)fetchAndSaveDemuxedData {
233234
[self saveSampleBuffer:videoBuffer];
234235
CFRelease(videoBuffer);
235236
}
237+
238+
NSTimeInterval deley = 1000 / self.frameRate;
239+
usleep(deley * 1000);
236240
}
237241
if (self.demuxerStatus == KFMP4DemuxerStatusCompleted) {
238242
NSLog(@"KFMP4Demuxer complete");
@@ -391,6 +395,9 @@ - (void)_setupDemuxReader:(NSError**)error {
391395
AVAssetTrack *videoTrack = [[self.config.asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
392396
_hasVideoTrack = videoTrack ? YES : NO;
393397
if (_hasVideoTrack) {
398+
// Get frame rate
399+
self.frameRate = videoTrack.nominalFrameRate > 0 ? videoTrack.nominalFrameRate : 30;
400+
394401
// Get the image transformation information.
395402
_preferredTransform = videoTrack.preferredTransform;
396403

0 commit comments

Comments
 (0)