Skip to content

refactor(session-replay): add separate method to get video info with more logging #5132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 25, 2025
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Improvements

- More logging for Session Replay video info (#5132)
- Improve session replay frame presentation timing calculations (#5133)
- Use wider compatible video encoding options for Session Replay (#5134)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation
import UIKit

// swiftlint:disable type_body_length
@objcMembers
class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {

Expand Down Expand Up @@ -217,21 +218,39 @@

group.enter()
videoWriter.inputs.forEach { $0.markAsFinished() }
videoWriter.finishWriting {
videoWriter.finishWriting { [weak self] in
defer { group.leave() }
if videoWriter.status == .completed {

SentryLog.debug("[Session Replay] Finished video writing, status: \(videoWriter.status)")
guard let strongSelf = self else {
SentryLog.warning("[Session Replay] On-demand replay is deallocated, completing writing session without output video info")
return

Check warning on line 227 in Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift#L226-L227

Added lines #L226 - L227 were not covered by tests
}

switch videoWriter.status {
case .writing:
SentryLog.error("[Session Replay] Finish writing video was called with status writing, this is unexpected! Completing with no video info")
case .cancelled:
SentryLog.warning("[Session Replay] Finish writing video was cancelled, completing with no video info.")
case .completed:
SentryLog.debug("[Session Replay] Finish writing video was completed, creating video info from file attributes.")
do {
let fileAttributes = try FileManager.default.attributesOfItem(atPath: outputFileURL.path)
guard let fileSize = fileAttributes[FileAttributeKey.size] as? Int else {
finishError = SentryOnDemandReplayError.cantReadVideoSize
return
}
guard let start = usedFrames.min(by: { $0.time < $1.time })?.time else { return }
let duration = TimeInterval(usedFrames.count / self.frameRate)
result = SentryVideoInfo(path: outputFileURL, height: Int(videoHeight), width: Int(videoWidth), duration: duration, frameCount: usedFrames.count, frameRate: self.frameRate, start: start, end: start.addingTimeInterval(duration), fileSize: fileSize, screens: usedFrames.compactMap({ $0.screenName }))
result = try strongSelf.getVideoInfo(
from: outputFileURL,
usedFrames: usedFrames,
videoWidth: Int(videoWidth),
videoHeight: Int(videoHeight)
)
} catch {
SentryLog.warning("[Session Replay] Failed to create video info from file attributes, reason: \(error.localizedDescription)")
finishError = error
}
case .failed, .unknown:
SentryLog.warning("[Session Replay] Finish writing video failed, reason: \(videoWriter.error?.localizedDescription ?? "Unknown error")")
finishError = videoWriter.error ?? SentryOnDemandReplayError.errorRenderingVideo
@unknown default:
SentryLog.warning("[Session Replay] Finish writing video failed, reason: \(videoWriter.error?.localizedDescription ?? "Unknown error")")
finishError = videoWriter.error ?? SentryOnDemandReplayError.errorRenderingVideo
}
}
group.wait()
Expand All @@ -242,13 +261,42 @@

private func filterFrames(beginning: Date, end: Date) -> [SentryReplayFrame] {
var frames = [SentryReplayFrame]()
//Using dispatch queue as sync mechanism since we need a queue already to generate the video.
workingQueue.dispatchSync({
// Using dispatch queue as sync mechanism since we need a queue already to generate the video.
workingQueue.dispatchSync {
frames = self._frames.filter { $0.time >= beginning && $0.time <= end }
})
}
return frames
}

fileprivate func getVideoInfo(from outputFileURL: URL, usedFrames: [SentryReplayFrame], videoWidth: Int, videoHeight: Int) throws -> SentryVideoInfo {
SentryLog.debug("[Session Replay] Getting video info from file: \(outputFileURL.path), width: \(videoWidth), height: \(videoHeight), used frames count: \(usedFrames.count)")
let fileAttributes = try FileManager.default.attributesOfItem(atPath: outputFileURL.path)
guard let fileSize = fileAttributes[FileAttributeKey.size] as? Int else {
SentryLog.warning("[Session Replay] Failed to read video size from video file, reason: size attribute not found")
throw SentryOnDemandReplayError.cantReadVideoSize

Check warning on line 276 in Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift#L275-L276

Added lines #L275 - L276 were not covered by tests
}
let minFrame = usedFrames.min(by: { $0.time < $1.time })
guard let start = minFrame?.time else {
// Note: This code path is currently not reached, because the `getVideoInfo` method is only called after the video is successfully created, therefore at least one frame was used.
// The compiler still requires us to unwrap the optional value, and we do not permit force-unwrapping.
SentryLog.warning("[Session Replay] Failed to read video start time from used frames, reason: no frames found")
throw SentryOnDemandReplayError.cantReadVideoStartTime

Check warning on line 283 in Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift#L280-L283

Added lines #L280 - L283 were not covered by tests
}
let duration = TimeInterval(usedFrames.count / self.frameRate)
return SentryVideoInfo(
path: outputFileURL,
height: videoHeight,
width: videoWidth,
duration: duration,
frameCount: usedFrames.count,
frameRate: self.frameRate,
start: start,
end: start.addingTimeInterval(duration),
fileSize: fileSize,
screens: usedFrames.compactMap({ $0.screenName })
)
}

internal func createVideoSettings(width: CGFloat, height: CGFloat) -> [String: Any] {
return [
// The codec type for the video. H.264 (AVC) is the most widely supported codec across platforms,
Expand Down Expand Up @@ -329,6 +377,7 @@
return NSValue(time: presentTime)
}
}
// swiftlint:enable type_body_length

#endif // os(iOS) || os(tvOS)
#endif // canImport(UIKit)
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,22 @@ class SentryOnDemandReplayTests: XCTestCase {
XCTAssertEqual(secondVideo.height, 10)
}

func testGenerateVideoInfo_whenNoFramesAdded_shouldNotThrowError() throws {
// -- Arrange --
let sut = getSut()
dateProvider.driftTimeForEveryRead = true
dateProvider.driftTimeInterval = 1

// -- Act --
let videos = try sut.createVideoWith(
beginning: Date(timeIntervalSinceReferenceDate: 0),
end: Date(timeIntervalSinceReferenceDate: 10)
)

// -- Assert --
XCTAssertNil(videos.first)
}

func testCalculatePresentationTime_withOneFPS_shouldReturnTiming() {
// -- Arrange --
let framesPerSecond = 1
Expand Down
Loading