Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Extract video processing to a new class (#5604)

### Fixes

- Fix video replay crashes due to video writer inputs not marked as finished on cancellation (#5608)

## 8.53.2

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
SentrySDKLog.debug("[Session Replay] Video writer input is ready, status: \(videoWriter.status)")
guard videoWriter.status == .writing else {
SentrySDKLog.error("[Session Replay] Video writer is not writing anymore, cancelling the writing session, reason: \(videoWriter.error?.localizedDescription ?? "Unknown error")")
videoWriter.inputs.forEach { $0.markAsFinished() }

Check warning on line 52 in Sources/Swift/Integrations/SessionReplay/SentryVideoFrameProcessor.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/SessionReplay/SentryVideoFrameProcessor.swift#L52

Added line #L52 was not covered by tests
videoWriter.cancelWriting()
return onCompletion(.failure(videoWriter.error ?? SentryOnDemandReplayError.errorRenderingVideo))
}
Expand Down Expand Up @@ -80,6 +81,7 @@
).timeValue
guard currentPixelBuffer.append(image: image, presentationTime: presentTime) else {
SentrySDKLog.error("[Session Replay] Failed to append image to pixel buffer, cancelling the writing session, reason: \(String(describing: videoWriter.error))")
videoWriter.inputs.forEach { $0.markAsFinished() }

Check warning on line 84 in Sources/Swift/Integrations/SessionReplay/SentryVideoFrameProcessor.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/SessionReplay/SentryVideoFrameProcessor.swift#L84

Added line #L84 was not covered by tests
videoWriter.cancelWriting()
return onCompletion(.failure(videoWriter.error ?? SentryOnDemandReplayError.errorRenderingVideo))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class SentryVideoFrameProcessorTests: XCTestCase {
var errorOverride: Error?
var cancelWritingCalled = false
var finishWritingCalled = false
var markAsFinishedCalled = false
var trackedInputs: [AVAssetWriterInput] = []

override var status: AVAssetWriter.Status {
return statusOverride
Expand All @@ -48,7 +48,11 @@ class SentryVideoFrameProcessorTests: XCTestCase {
}

override var inputs: [AVAssetWriterInput] {
return []
return trackedInputs
}

override func add(_ input: AVAssetWriterInput) {
trackedInputs.append(input)
}
}

Expand Down Expand Up @@ -77,6 +81,7 @@ class SentryVideoFrameProcessorTests: XCTestCase {
var requestMediaDataWhenReadyCalled = false
var requestMediaDataWhenReadyQueue: DispatchQueue?
var requestMediaDataWhenReadyBlock: (() -> Void)?
var markAsFinishedInvocations = Invocations<Void>()

override var isReadyForMoreMediaData: Bool {
return isReadyForMoreMediaDataOverride
Expand All @@ -89,7 +94,7 @@ class SentryVideoFrameProcessorTests: XCTestCase {
}

override func markAsFinished() {
// No-op for testing
markAsFinishedInvocations.record(Void())
}
}

Expand Down Expand Up @@ -207,12 +212,14 @@ class SentryVideoFrameProcessorTests: XCTestCase {
func testProcessFrames_WhenVideoWriterNotWriting_ShouldCancelWriting() {
let sut = fixture.getSut()
let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
fixture.videoWriter.add(videoWriterInput)
fixture.videoWriter.statusOverride = .failed
let completionInvocations = Invocations<Result<SentryRenderVideoResult, any Error>>()

sut.processFrames(videoWriterInput: videoWriterInput) { completionInvocations.record($0) }

XCTAssertTrue(fixture.videoWriter.cancelWritingCalled)
XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)

let result = completionInvocations.invocations.first
Expand All @@ -229,6 +236,7 @@ class SentryVideoFrameProcessorTests: XCTestCase {
func testProcessFrames_WhenNoMoreFrames_ShouldFinishVideo() {
let sut = fixture.getSut()
let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
fixture.videoWriter.add(videoWriterInput)
let completionInvocations = Invocations<Result<SentryRenderVideoResult, any Error>>()

// Process all frames
Expand All @@ -240,11 +248,13 @@ class SentryVideoFrameProcessorTests: XCTestCase {
sut.processFrames(videoWriterInput: videoWriterInput) { completionInvocations.record($0) }

XCTAssertTrue(fixture.videoWriter.finishWritingCalled)
XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)
}

func testProcessFrames_WhenImageSizeChanges_ShouldFinishVideo() {
let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
fixture.videoWriter.add(videoWriterInput)
let completionInvocations = Invocations<Result<SentryRenderVideoResult, any Error>>()
let sutWithDifferentSize = SentryVideoFrameProcessor(
videoFrames: fixture.videoFrames,
Expand All @@ -261,19 +271,22 @@ class SentryVideoFrameProcessorTests: XCTestCase {
sutWithDifferentSize.processFrames(videoWriterInput: videoWriterInput) { completionInvocations.record($0) }

XCTAssertTrue(fixture.videoWriter.finishWritingCalled)
XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)
}

func testProcessFrames_WhenPixelBufferAppendFails_ShouldCancelWriting() {
let sut = fixture.getSut()
let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
fixture.videoWriter.add(videoWriterInput)
fixture.currentPixelBuffer.appendShouldReturn = false
let completionInvocations = Invocations<Result<SentryRenderVideoResult, any Error>>()

// Process all frames
sut.processFrames(videoWriterInput: videoWriterInput) { completionInvocations.record($0) }

XCTAssertTrue(fixture.videoWriter.cancelWritingCalled)
XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)

let result = completionInvocations.invocations.first
Expand Down Expand Up @@ -319,6 +332,8 @@ class SentryVideoFrameProcessorTests: XCTestCase {

func testFinishVideo_WhenWriterCompleted_ShouldReturnVideoInfo() {
let sut = fixture.getSut()
let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
fixture.videoWriter.add(videoWriterInput)
fixture.videoWriter.statusOverride = .completed
let completionInvocations = Invocations<Result<SentryRenderVideoResult, any Error>>()

Expand All @@ -333,6 +348,7 @@ class SentryVideoFrameProcessorTests: XCTestCase {
completionInvocations.record(result)
})

XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)

let result = completionInvocations.invocations.first
Expand All @@ -356,13 +372,16 @@ class SentryVideoFrameProcessorTests: XCTestCase {

func testFinishVideo_WhenWriterCancelled_ShouldReturnNilVideoInfo() {
let sut = fixture.getSut()
let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
fixture.videoWriter.add(videoWriterInput)
fixture.videoWriter.statusOverride = .cancelled
let completionInvocations = Invocations<Result<SentryRenderVideoResult, any Error>>()

sut.finishVideo(frameIndex: 2, onCompletion: { result in
completionInvocations.record(result)
})

XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)

let result = completionInvocations.invocations.first
Expand All @@ -380,6 +399,8 @@ class SentryVideoFrameProcessorTests: XCTestCase {

func testFinishVideo_WhenWriterFailed_ShouldReturnError() {
let sut = fixture.getSut()
let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
fixture.videoWriter.add(videoWriterInput)
fixture.videoWriter.statusOverride = .failed
fixture.videoWriter.errorOverride = SentryOnDemandReplayError.errorRenderingVideo
let completionInvocations = Invocations<Result<SentryRenderVideoResult, any Error>>()
Expand All @@ -388,6 +409,7 @@ class SentryVideoFrameProcessorTests: XCTestCase {
completionInvocations.record(result)
})

XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)

let result = completionInvocations.invocations.first
Expand All @@ -404,6 +426,8 @@ class SentryVideoFrameProcessorTests: XCTestCase {
func testFinishVideo_WhenSelfIsDeallocated_ShouldReturnNilVideoInfo() {
let delayedVideoWriter = try! DelayedTestAVAssetWriter(url: fixture.outputFileURL, fileType: .mp4)
delayedVideoWriter.statusOverride = .completed
let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
delayedVideoWriter.add(videoWriterInput)

// Create a weak reference to track deallocation
weak var weakSut: SentryVideoFrameProcessor?
Expand Down Expand Up @@ -439,6 +463,7 @@ class SentryVideoFrameProcessorTests: XCTestCase {
delayedVideoWriter.executeCompletion()

// Verify the completion was called with nil video info
XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)

let result = completionInvocations.invocations.first
Expand All @@ -456,6 +481,8 @@ class SentryVideoFrameProcessorTests: XCTestCase {

func testFinishVideo_WhenOutputFileDoesNotExist_ShouldReturnError() {
let sut = fixture.getSut()
let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
fixture.videoWriter.add(videoWriterInput)
fixture.videoWriter.statusOverride = .completed
let completionInvocations = Invocations<Result<SentryRenderVideoResult, any Error>>()

Expand All @@ -469,6 +496,7 @@ class SentryVideoFrameProcessorTests: XCTestCase {
completionInvocations.record(result)
})

XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)

let result = completionInvocations.invocations.first
Expand Down Expand Up @@ -556,16 +584,19 @@ class SentryVideoFrameProcessorTests: XCTestCase {
)

let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
fixture.videoWriter.add(videoWriterInput)
sut.processFrames(videoWriterInput: videoWriterInput) { completionInvocations.record($0) }

XCTAssertTrue(fixture.videoWriter.finishWritingCalled)
XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)
XCTAssertEqual(sut.frameIndex, 0)
}

func testProcessFrames_WithLargeFrameIndex_ShouldFinishImmediately() {
let sut = fixture.getSut()
let videoWriterInput = TestAVAssetWriterInput(mediaType: .video, outputSettings: nil)
fixture.videoWriter.add(videoWriterInput)
let completionInvocations = Invocations<Result<SentryRenderVideoResult, any Error>>()

// Set frame index beyond available frames
Expand All @@ -574,6 +605,7 @@ class SentryVideoFrameProcessorTests: XCTestCase {
sut.processFrames(videoWriterInput: videoWriterInput) { completionInvocations.record($0) }

XCTAssertTrue(fixture.videoWriter.finishWritingCalled)
XCTAssertEqual(videoWriterInput.markAsFinishedInvocations.count, 1)
XCTAssertEqual(completionInvocations.count, 1)
XCTAssertEqual(sut.frameIndex, 10)
}
Expand Down
Loading