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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Improvements

- Add separate method to get video info with more logging (#5132)

## 8.49.1

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,21 +212,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 221 in Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift#L220-L221

Added lines #L220 - L221 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 @@ -237,13 +255,43 @@

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 270 in Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift#L269-L270

Added lines #L269 - L270 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 278 in Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift#L274-L278

Added lines #L274 - L278 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 })
)
}

private func createVideoSettings(width: CGFloat, height: CGFloat) -> [String: Any] {
return [
AVVideoCodecKey: AVVideoCodecType.h264,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,21 @@ class SentryOnDemandReplayTests: XCTestCase {
XCTAssertEqual(secondVideo.width, 20)
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)
}
}
#endif
Loading