Skip to content

Commit

Permalink
Keep only one timer instead of repeatedly creating and destroy new on…
Browse files Browse the repository at this point in the history
…e in VideoFrameResender
  • Loading branch information
georgezy-amzn committed Jan 30, 2025
1 parent 257e779 commit 5180a5d
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import ReplayKit
private let logger: Logger
private let sinks = ConcurrentMutableSet()

private lazy var videoFrameResender = VideoFrameResender(minFrameRate: 5) { [weak self] (frame) -> Void in
private lazy var videoFrameResender = VideoFrameResender(minFrameRate: 5, logger: self.logger) { [weak self] (frame) -> Void in
guard let `self` = self else { return }
self.sendVideoFrame(frame: frame)
}
Expand Down
120 changes: 65 additions & 55 deletions AmazonChimeSDK/AmazonChimeSDK/internal/video/VideoFrameResender.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,88 +13,98 @@ import CoreMedia
/// This can be useful with sources which may pause the generation of frames (like in-app ReplayKit screen sharing)
/// so that internally encoders don't get in a poor state, and new receivers can immediately receive frames
@objcMembers public class VideoFrameResender: NSObject {
private let minFrameRate: UInt
private let resendQueue = DispatchQueue.global()

// Create a serial queue for resending video frame
private let resendQueue = DispatchQueue(label: "com.amazonaws.services.chime.VideoFrameResender")

// Will be nil until the first frame is sent, and nil again on stop
private var resendTimer: DispatchSourceTimer?
private let resendTimer: DispatchSourceTimer

// Cached constant values
private let resendTimeInterval: CMTime
private let resendScheduleLeewayMs = DispatchTimeInterval.milliseconds(20)


private let logger: Logger

private let lock = NSLock()

private var lastSendTimestamp: CMTime?
private var lastVideoFrame: VideoFrame?

private let resendFrameHandler: (VideoFrame) -> Void

/// Callback will be triggered with a previous `VideoFrame` which will have different timestamp
/// than originally sent with so it won't be dropped by downstream encoders
init(minFrameRate: UInt, resendFrameHandler: @escaping (VideoFrame) -> Void) {
self.minFrameRate = minFrameRate
self.resendFrameHandler = resendFrameHandler
self.resendTimeInterval = CMTime(value: CMTimeValue(Constants.millisecondsPerSecond / Int(minFrameRate)),
timescale: CMTimeScale(Constants.millisecondsPerSecond))
}

func stop() {
resendTimer?.cancel()
resendTimer = nil
}

/// Calling this function will kick off a timer which will begin checking if frames need to be resent
/// to maintain a minimum frame frame
func frameDidSend(videoFrame: VideoFrame) {
lastSendTimestamp = CMClockGetTime(CMClockGetHostTimeClock())
lastVideoFrame = videoFrame

if let resendTimer = resendTimer,
resendTimer.isCancelled == false {
// There is already a timer running
return
}
let timer = DispatchSource.makeTimerSource(flags: .strict, queue: resendQueue)
resendTimer = timer

// This timer is invoked every resendTimeInterval when no frame is sent from video source
timer.setEventHandler(handler: { [weak self] in
init(minFrameRate: UInt,
logger: Logger,
resendFrameHandler: @escaping (VideoFrame) -> Void) {
self.resendTimer = DispatchSource.makeTimerSource(flags: .strict, queue: resendQueue)
self.logger = logger

super.init()

let minFrameInterval = Constants.millisecondsPerSecond / Int(minFrameRate)
let resendTimeInterval:CMTime = CMTime(value: CMTimeValue(minFrameInterval),
timescale: CMTimeScale(Constants.millisecondsPerSecond))

self.resendTimer.setEventHandler(handler: { [weak self] in
guard let `self` = self else {
timer.cancel()
self?.resendTimer.cancel()
self?.logger.error(msg: "Unable to resend video frame, VideoFrameResender is unavailable. Thread: \(Thread.current)")
return
}


self.logger.info(msg: "Checking if there is pending frame for resending. Thread: \(Thread.current)")
lock.lock()
guard let lastSendTimestamp = self.lastSendTimestamp,
let lastVideoFrame = self.lastVideoFrame else { return }
lock.unlock()

self.logger.info(msg: "Checking the time elapsed for resending. Thread: \(Thread.current)")
let currentTimestamp = CMClockGetTime(CMClockGetHostTimeClock())
let delta = CMTimeSubtract(currentTimestamp, lastSendTimestamp)

// Resend the last input frame if there is no new input frame after resendTimeInterval
if delta > self.resendTimeInterval {
if delta > resendTimeInterval {
self.logger.info(msg: "Creating new video frame for resending. Thread: \(Thread.current)")
// Update the timestamp so it's not dropped by downstream as a duplicate
let lastVideoFrameTime = CMTimeMake(value: lastVideoFrame.timestampNs,
timescale: Int32(Constants.nanosecondsPerSecond))
let newVideoFrame = VideoFrame(timestampNs: Int64(CMTimeAdd(lastVideoFrameTime, delta).seconds
* Double(Constants.nanosecondsPerSecond)),
rotation: lastVideoFrame.rotation,
buffer: lastVideoFrame.buffer)

// Cancel the current timer so that new frames will kick it off again
self.resendTimer?.cancel()
self.resendTimer = nil

self.resendFrameHandler(newVideoFrame)
} else {
// Reset resending schedule if there is an input frame between internals
let remainingSeconds = self.resendTimeInterval.seconds - delta.seconds
let deadline = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(remainingSeconds *
Double(Constants.millisecondsPerSecond)))
self.resendTimer?.schedule(deadline: deadline, leeway: self.resendScheduleLeewayMs)
self.logger.info(msg: "Resending last frame. Thread: \(Thread.current)")
resendFrameHandler(newVideoFrame)
}

})

self.resendTimer.schedule(deadline: .now(),
repeating: DispatchTimeInterval.milliseconds(minFrameInterval),
leeway: resendScheduleLeewayMs)

self.resendTimer.activate()
}

deinit {
// Call stop on deinit to cancel`resendTimer` and queued tasks in `resendQueue`
self.stop()
}

func stop() {
// Stop `resendTimer` from scheduling in tasks
self.resendTimer.cancel()

// This will stop the queued tasks in resendQueue from running
lock.lock()
self.lastSendTimestamp = nil
self.lastVideoFrame = nil
lock.unlock()
}

let deadline = DispatchTime.now()
+ DispatchTimeInterval.milliseconds(Constants.millisecondsPerSecond / Int(minFrameRate))
timer.schedule(deadline: deadline, leeway: resendScheduleLeewayMs)
timer.activate()
/// Calling this function will kick off a timer which will begin checking if frames need to be resent
/// to maintain a minimum frame frame
func frameDidSend(videoFrame: VideoFrame) {
lock.lock()
lastSendTimestamp = CMClockGetTime(CMClockGetHostTimeClock())
lastVideoFrame = videoFrame
lock.unlock()
}
}

0 comments on commit 5180a5d

Please sign in to comment.