Skip to content

Commit

Permalink
Update pip tracks when app becomes active again
Browse files Browse the repository at this point in the history
  • Loading branch information
ipavlidakis committed Dec 18, 2024
1 parent 8429948 commit 12087c5
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ extension Stream_Video_Sfu_Signal_TrackSubscriptionDetails {
type: Stream_Video_Sfu_Models_TrackType
) {
userID = userId
dimension = size.map { Stream_Video_Sfu_Models_VideoDimension($0) }
?? Stream_Video_Sfu_Models_VideoDimension()
if type == .video || type == .screenShare {
dimension = size.map { Stream_Video_Sfu_Models_VideoDimension($0) } ?? Stream_Video_Sfu_Models_VideoDimension()
}
sessionID = sessionId
trackType = type
}
Expand Down
1 change: 1 addition & 0 deletions Sources/StreamVideo/WebRTC/v2/SFU/SFUAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ final class SFUAdapter: ConnectionStateDelegate, CustomStringConvertible, @unche

try Task.checkCancellation()

log.debug(request, subsystems: .sfu)
let task = Task { [request, signalService] in
try Task.checkCancellation()
return try await executeTask(retryPolicy: .neverGonnaGiveYouUp { true }) {
Expand Down
36 changes: 36 additions & 0 deletions Sources/StreamVideoSwiftUI/CallViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ open class CallViewModel: ObservableObject {
private var recordingUpdates: AnyCancellable?
private var screenSharingUpdates: AnyCancellable?
private var callSettingsUpdates: AnyCancellable?
private var applicationLifecycleUpdates: AnyCancellable?

private var ringingTimer: Foundation.Timer?
private var lastScreenSharingParticipant: CallParticipant?
Expand Down Expand Up @@ -216,6 +217,7 @@ open class CallViewModel: ObservableObject {
localCallSettingsChange = callSettings != nil

subscribeToCallEvents()
subscribeToApplicationLifecycleEvents()
pictureInPictureAdapter.onSizeUpdate = { [weak self] in
self?.updateTrackSize($0, for: $1)
}
Expand Down Expand Up @@ -835,6 +837,40 @@ open class CallViewModel: ObservableObject {
private func participantAutoLeavePolicyTriggered() {
leaveCall()
}

private func subscribeToApplicationLifecycleEvents() {
#if canImport(UIKit)
/// If we are running on a UIKit application, we observe the application state in order to disable
/// PictureInPicture when active but the app is in foreground.
applicationLifecycleUpdates = NotificationCenter.default
.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in self?.applicationDidBecomeActive() }
log.debug("\(type(of: self)) now observes application lifecycle.")
#endif
}

private func applicationDidBecomeActive() {
guard let call else { return }

let tracksToBeActivated = call
.state
.participants
.filter { $0.hasVideo && $0.track?.isEnabled == false }

guard !tracksToBeActivated.isEmpty else {
log.debug("\(type(of: self)) application lifecycle observer found no tracks to activate.")
return
}

log.debug(
"""
\(tracksToBeActivated.count) tracks have been deactivate while in background
and now the app is active need to be activated again.
"""
)

tracksToBeActivated.forEach { $0.track?.isEnabled = true }
}
}

/// The state of the call.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
/// A size ratio threshold used to determine if skipping frames is required.
private let sizeRatioThreshold: CGFloat = 15

private let isLoggingEnabled = false

// MARK: - Lifecycle

@available(*, unavailable)
Expand Down Expand Up @@ -131,25 +133,34 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
// has changed.
trackSize = .init(width: Int(frame.width), height: Int(frame.height))

log.debug("→ Received frame with trackSize:\(trackSize)", subsystems: .pictureInPicture)
logMessage(
.debug,
message: "→ Received frame with trackSize:\(trackSize)"
)

defer {
handleFrameSkippingIfRequired()
}

guard shouldRenderFrame else {
log.debug("→ Skipping frame.")
logMessage(.debug, message: "→ Skipping frame.")
return
}

if
let yuvBuffer = bufferTransformer.transformAndResizeIfRequired(frame, targetSize: contentSize)?
.buffer as? StreamRTCYUVBuffer,
let sampleBuffer = yuvBuffer.sampleBuffer {
log.debug("➕ Buffer for trackId:\(track?.trackId ?? "n/a") added.", subsystems: .pictureInPicture)
logMessage(
.debug,
message: "➕ Buffer for trackId:\(track?.trackId ?? "n/a") added."
)
bufferPublisher.send(sampleBuffer)
} else {
log.warning("Failed to convert \(type(of: frame.buffer)) CMSampleBuffer.", subsystems: .pictureInPicture)
logMessage(
.warning,
message: "Failed to convert \(type(of: frame.buffer)) CMSampleBuffer."
)
}
}

Expand All @@ -174,21 +185,24 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
buffer.isValid
else {
contentView.renderingComponent.flush()
log.debug("🔥 Display layer flushed.", subsystems: .pictureInPicture)
logMessage(.debug, message: "🔥 Display layer flushed.")
return
}

log.debug("⚙️ Processing buffer for trackId:\(trackId).", subsystems: .pictureInPicture)
logMessage(
.debug,
message: "⚙️ Processing buffer for trackId:\(trackId)."
)
if #available(iOS 14.0, *) {
if contentView.renderingComponent.requiresFlushToResumeDecoding == true {
contentView.renderingComponent.flush()
log.debug("🔥 Display layer for track:\(trackId) flushed.", subsystems: .pictureInPicture)
logMessage(.debug, message: "🔥 Display layer for track:\(trackId) flushed.")
}
}

if contentView.renderingComponent.isReadyForMoreMediaData {
contentView.renderingComponent.enqueue(buffer)
log.debug("✅ Buffer for trackId:\(trackId) enqueued.", subsystems: .pictureInPicture)
logMessage(.debug, message: "✅ Buffer for trackId:\(trackId) enqueued.")
}
}

Expand All @@ -206,7 +220,10 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
.sink { [weak self] in self?.process($0) }

track.add(self)
log.debug("⏳ Frame streaming for Picture-in-Picture started.", subsystems: .pictureInPicture)
logMessage(
.debug,
message: "⏳ Frame streaming for Picture-in-Picture started."
)
}

/// A method that stops the frame consumption from the track. Used automatically when the rendering
Expand All @@ -217,7 +234,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
bufferUpdatesCancellable = nil
track?.remove(self)
contentView.renderingComponent.flush()
log.debug("Frame streaming for Picture-in-Picture stopped.", subsystems: .pictureInPicture)
logMessage(.debug, message: "Frame streaming for Picture-in-Picture stopped.")
}

/// A method used to calculate rendering required properties, every time the trackSize changes.
Expand All @@ -239,7 +256,9 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
/// to the value that fits.
pictureInPictureWindowSizePolicy.trackSize = trackSize

log.debug(
logMessage(
.debug,
message:
"""
contentSize:\(contentSize)
trackId:\(track?.trackId ?? "n/a")
Expand All @@ -249,7 +268,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
skippedFrames:\(skippedFrames)
widthDiffRatio:\(widthDiffRatio)
heightDiffRatio:\(heightDiffRatio)
""", subsystems: .pictureInPicture
"""
)
}

Expand All @@ -261,9 +280,9 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
} else {
skippedFrames += 1
}
log.debug(
"noOfFramesToSkipAfterRendering:\(noOfFramesToSkipAfterRendering) skippedFrames:\(skippedFrames)",
subsystems: .pictureInPicture
logMessage(
.debug,
message: "noOfFramesToSkipAfterRendering:\(noOfFramesToSkipAfterRendering) skippedFrames:\(skippedFrames)"
)
} else if skippedFrames > 0 {
skippedFrames = 0
Expand All @@ -278,4 +297,26 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
requiresResize = false
startFrameStreaming(for: track, on: window)
}

private func logMessage(
_ level: LogLevel,
message: String,
error: Error? = nil,
file: StaticString = #file,
functionName: StaticString = #function,
line: UInt = #line
) {
guard isLoggingEnabled else {
return
}
log.log(
level,
functionName: functionName,
fileName: file,
lineNumber: line,
message: message,
subsystems: .pictureInPicture,
error: error
)
}
}

0 comments on commit 12087c5

Please sign in to comment.