diff --git a/CHANGELOG.md b/CHANGELOG.md index fbaead2..76e3f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2.10.0 +- **FEAT**(iOS, macOS): Add `VideoRenderData.trimToCommonTrackEnd` (default `false`). Ends each clip where both its video and audio track still have content, so an export no longer finishes on a stretch of missing audio — the seam a looping player replays every cycle. Leave it off when a clip is meant to outlast its own audio. + ## 2.9.0 - **FEAT**(android): `RenderEncoderException` now carries `isTransient`. A render that failed because the device's codec resources were exhausted is flagged as retryable, so it can be told apart from a genuine format/encoder incompatibility, which is not. Covers a starved decoder as well as a starved encoder. - **FIX**(android): The software-encoder fallback no longer silently re-runs the hardware encoder under a `software-encoder` label. It is skipped with an explicit log line when the device has no usable software encoder. diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift index 7a83c88..2b91f47 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift @@ -205,7 +205,8 @@ class RenderVideo { videoClips: workingConfig.videoClips, videoEffects: effectsConfig, enableAudio: workingConfig.enableAudio, - audioTracks: workingConfig.audioTracks + audioTracks: workingConfig.audioTracks, + trimToCommonTrackEnd: workingConfig.trimToCommonTrackEnd ) } let ( @@ -507,8 +508,24 @@ class RenderVideo { export.outputURL = outputURL export.outputFileType = mapFormatToMimeType(format: config.outputFormat) export.shouldOptimizeForNetworkUse = config.shouldOptimizeForNetworkUse + // Attached before the track loads below so a cancel arriving during them + // is still honoured; `export()` only starts in `monitorExportProgress`. handle.attach(export: export) + // This fast path skips the composition entirely, so it has to apply the + // common-track-end trim itself — otherwise the flag would be a silent + // no-op for exactly the single untrimmed clip it targets. Passthrough + // honours `timeRange` frame-accurately (same as the split fast path), so + // the export stays lossless. + if config.trimToCommonTrackEnd, config.enableAudio, + let trimmed = await TrackEndTrimmer.trimmedAssetRange(of: asset, label: "Passthrough") + { + export.timeRange = trimmed + PluginLog.print( + " ✂️ Passthrough trimmed to common track end: " + + "\(String(format: "%.3f", trimmed.duration.seconds))s") + } + do { try await monitorExportProgress(export, onProgress: onProgress) } catch { diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyComposition.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyComposition.swift index cdec23b..064d591 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyComposition.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyComposition.swift @@ -27,13 +27,15 @@ func applyComposition( videoClips: [VideoClip], videoEffects: VideoCompositorConfig, enableAudio: Bool, - audioTracks: [AudioTrackConfig] + audioTracks: [AudioTrackConfig], + trimToCommonTrackEnd: Bool = false ) async throws -> ( AVMutableComposition, VideoCompositionData, CGSize, AVAudioMix?, CMPersistentTrackID, [URL], [FadeWindow] ) { return try await CompositionBuilder(videoClips: videoClips, videoEffects: videoEffects) .setEnableAudio(enableAudio) + .setTrimToCommonTrackEnd(trimToCommonTrackEnd) .setAudioTracks(audioTracks) .build() } diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift index 08b9ecf..f7cb897 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift @@ -12,6 +12,7 @@ internal class CompositionBuilder { private let videoClips: [VideoClip] private let videoEffects: VideoCompositorConfig private var enableAudio: Bool = true + private var trimToCommonTrackEnd: Bool = false private var audioTracks: [AudioTrackConfig] = [] /// Initializes builder with configuration. @@ -33,6 +34,16 @@ internal class CompositionBuilder { return self } + /// Ends each clip where both of its tracks still have content. + /// + /// - Parameter enabled: If true, a clip is cut back to the earlier of its + /// video and audio track ends instead of spanning the longer one + /// - Returns: Self for chaining + func setTrimToCommonTrackEnd(_ enabled: Bool) -> CompositionBuilder { + self.trimToCommonTrackEnd = enabled + return self + } + /// Sets the audio tracks for mixing. /// /// - Parameter tracks: Array of audio track configurations @@ -66,6 +77,7 @@ internal class CompositionBuilder { // Build video sequence let videoBuilder = VideoSequenceBuilder(videoClips: videoClips) .setEnableAudio(enableAudio) + .setTrimToCommonTrackEnd(trimToCommonTrackEnd) let videoResult = try await videoBuilder.build(in: composition) diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/TrackEndTrimmer.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/TrackEndTrimmer.swift new file mode 100644 index 0000000..ef1443d --- /dev/null +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/TrackEndTrimmer.swift @@ -0,0 +1,112 @@ +import AVFoundation +import Foundation + +/// Cuts a clip range back to where an asset's video and audio tracks both +/// still have content. +/// +/// A source asset's two tracks routinely end tens of milliseconds apart, +/// because capture and export stop them independently. A range that spans the +/// longer track ends on missing audio — the seam a looping player replays +/// every cycle. +/// +/// Both render paths that can honour `trimToCommonTrackEnd` go through here: +/// the composition path (`VideoSequenceBuilder`, per clip) and the lossless +/// bitrate-cap fast path (`RenderVideo.passthroughExport`, whole asset). +internal enum TrackEndTrimmer { + + /// How far a clip's audio may fall short of its video before the gap counts + /// as content rather than a track-end mismatch. + /// + /// Capture and export pipelines end the two tracks a fraction of a second + /// apart; a survey of published mp4s put the worst case at ~0.4 s. A larger + /// gap means the clip is genuinely meant to outlast its audio, and trimming + /// it would swallow content instead of a seam. + static let maxTrackEndMismatch = CMTime(value: 500, timescale: 1000) + + /// The track's time range, loaded asynchronously where the OS supports it. + static func timeRange(of track: AVAssetTrack) async -> CMTimeRange { + #if os(iOS) + if #available(iOS 15.0, *) { + return (try? await track.load(.timeRange)) ?? track.timeRange + } + #elseif os(macOS) + if #available(macOS 13.0, *) { + return (try? await track.load(.timeRange)) ?? track.timeRange + } + #endif + return track.timeRange + } + + /// Returns `range` cut back to where `asset`'s audio track also has content, + /// or `nil` when the range should be left as it is. + /// + /// - Parameters: + /// - range: The clip range, already clamped to the video track + /// - asset: The source asset `range` refers to + /// - label: Identifies the clip in the log line for a skipped trim + /// - Returns: The trimmed range, or `nil` to keep `range` unchanged + static func trimmedRange( + _ range: CMTimeRange, + in asset: AVAsset, + label: String + ) async -> CMTimeRange? { + guard let audioTrack = try? await MediaInfoExtractor.loadAudioTrack(from: asset) else { + return nil + } + + // `CMTimeRangeGetIntersection` clamps both ends, but only the end moves in + // practice: AVFoundation folds a leading empty edit into the track's + // duration rather than into its start, so an audio track that begins late + // still reports `timeRange.start == 0` and the clip keeps its head. + let commonRange = CMTimeRangeGetIntersection( + range, otherRange: await timeRange(of: audioTrack)) + let shortfall = CMTimeSubtract(range.duration, commonRange.duration) + + if commonRange.duration > .zero, shortfall <= maxTrackEndMismatch { + return commonRange + } + + if shortfall > maxTrackEndMismatch { + // Too large to be an encoder tail: the clip genuinely outlasts its own + // audio (stop motion held past a short sound, a source file with a + // broken audio track). Trimming here would swallow content, so keep the + // video and leave the tail alone. + PluginLog.print( + " ⚠️ \(label) audio ends \(String(format: "%.2f", shortfall.seconds))s early — too far to treat as a track-end mismatch, not trimming" + ) + } + return nil + } + + /// Returns the whole asset cut back to where both tracks still have content, + /// or `nil` when it should be exported as it is. + /// + /// Mirrors what the composition path does for an untrimmed single clip: + /// clamp to the video track first, then to the audio track. + /// + /// - Parameters: + /// - asset: The source asset to measure + /// - label: Identifies the asset in the log line for a skipped trim + /// - Returns: The trimmed range, or `nil` to export the asset unchanged + static func trimmedAssetRange(of asset: AVAsset, label: String) async -> CMTimeRange? { + guard let videoTrack = try? await MediaInfoExtractor.loadVideoTrack(from: asset) else { + return nil + } + + let assetDuration: CMTime + if #available(iOS 15.0, macOS 13.0, *) { + assetDuration = (try? await asset.load(.duration)) ?? .zero + } else { + assetDuration = asset.duration + } + + // Some MP4 files have a container duration slightly longer than the video + // track's decoded frames; clamp to the track before consulting the audio. + let rawRange = CMTimeRange(start: .zero, duration: assetDuration) + let clamped = CMTimeRangeGetIntersection( + rawRange, otherRange: await timeRange(of: videoTrack)) + let videoRange = clamped.duration > .zero ? clamped : rawRange + + return await trimmedRange(videoRange, in: asset, label: label) + } +} diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/VideoSequenceBuilder.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/VideoSequenceBuilder.swift index e825679..b47124b 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/VideoSequenceBuilder.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/VideoSequenceBuilder.swift @@ -9,6 +9,7 @@ internal class VideoSequenceBuilder { private let videoClips: [VideoClip] private var enableAudio: Bool = true + private var trimToCommonTrackEnd: Bool = false /// Initializes builder with video clips. /// @@ -26,6 +27,16 @@ internal class VideoSequenceBuilder { return self } + /// Ends each clip where both of its tracks still have content. + /// + /// - Parameter enabled: If true, a clip is cut back to the earlier of its + /// video and audio track ends instead of spanning the longer one + /// - Returns: Self for chaining + func setTrimToCommonTrackEnd(_ enabled: Bool) -> VideoSequenceBuilder { + self.trimToCommonTrackEnd = enabled + return self + } + /// Calculates total duration of all video clips combined. /// /// - Returns: Total duration as CMTime @@ -180,23 +191,22 @@ internal class VideoSequenceBuilder { // the insert but the ClipInstruction keeps the longer duration, creating a gap // where AVFoundation calls the compositor with no source frame available // (sourceTrackIDs empty), causing a RENDER_ERROR crash. - let videoTrackTimeRange: CMTimeRange - #if os(iOS) - if #available(iOS 15.0, *) { - videoTrackTimeRange = (try? await videoTrack.load(.timeRange)) ?? videoTrack.timeRange - } else { - videoTrackTimeRange = videoTrack.timeRange - } - #elseif os(macOS) - if #available(macOS 13.0, *) { - videoTrackTimeRange = (try? await videoTrack.load(.timeRange)) ?? videoTrack.timeRange - } else { - videoTrackTimeRange = videoTrack.timeRange - } - #endif + let videoTrackTimeRange = await TrackEndTrimmer.timeRange(of: videoTrack) let clampedRange = CMTimeRangeGetIntersection( rawClipTimeRange, otherRange: videoTrackTimeRange) - let clipTimeRange = clampedRange.duration > .zero ? clampedRange : rawClipTimeRange + var clipTimeRange = clampedRange.duration > .zero ? clampedRange : rawClipTimeRange + + // The video clamp above leaves a range the audio track cannot fill when + // the two tracks end apart, `insertTimeRange` silently inserts what + // exists, and the export ends on a stretch of missing audio — the seam a + // looping player replays every cycle. Cut the clip back to where both + // tracks still have content instead. + if trimToCommonTrackEnd, enableAudio, + let trimmed = await TrackEndTrimmer.trimmedRange( + clipTimeRange, in: asset, label: "Clip \(index)") + { + clipTimeRange = trimmed + } let clipDuration = clipTimeRange.duration let insertStart = totalDuration let sourceRanges: [CMTimeRange] diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift index 5a3f635..6df6d97 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift @@ -340,6 +340,13 @@ struct RenderConfig: Sendable { /// Whether to include audio in output let enableAudio: Bool + /// Whether to end each clip where both of its tracks still have content + /// + /// A source asset's audio and video tracks routinely end tens of + /// milliseconds apart. Spanning the longer one leaves a tail of missing + /// audio or a frozen frame, which a looping player replays as a seam. + let trimToCommonTrackEnd: Bool + /// Playback speed multiplier (e.g., 2.0 = 2x speed) let playbackSpeed: Float? @@ -392,6 +399,7 @@ struct RenderConfig: Sendable { bitrate: self.bitrate, maxFrameRate: self.maxFrameRate, enableAudio: self.enableAudio, + trimToCommonTrackEnd: self.trimToCommonTrackEnd, playbackSpeed: self.playbackSpeed, colorFilters: self.colorFilters, audioTracks: self.audioTracks, @@ -461,6 +469,7 @@ struct RenderConfig: Sendable { bitrate: args["bitrate"] as? Int, maxFrameRate: (args["maxFrameRate"] as? NSNumber)?.intValue, enableAudio: args["enableAudio"] as? Bool ?? true, + trimToCommonTrackEnd: args["trimToCommonTrackEnd"] as? Bool ?? false, playbackSpeed: (args["playbackSpeed"] as? NSNumber)?.floatValue, colorFilters: colorFilters, audioTracks: audioTracks, diff --git a/example/lib/features/render/video_renderer_page.dart b/example/lib/features/render/video_renderer_page.dart index ec06734..e8da882 100644 --- a/example/lib/features/render/video_renderer_page.dart +++ b/example/lib/features/render/video_renderer_page.dart @@ -758,6 +758,15 @@ class _VideoRendererPageState extends State { await _renderVideo(data); } + Future _trimToCommonTrackEnd() async { + var data = VideoRenderData( + videoSegments: [VideoSegment(video: _video)], + trimToCommonTrackEnd: true, + ); + + await _renderVideo(data); + } + Future _writeAssetAudioToFile(String assetPath) async { final ByteData data = await rootBundle.load(assetPath); final buffer = data.buffer; @@ -2286,6 +2295,15 @@ class _VideoRendererPageState extends State { leading: const Icon(Icons.volume_off_outlined), title: const Text('Remove Audio'), ), + if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) + ListTile( + onTap: _trimToCommonTrackEnd, + leading: const Icon(Icons.content_cut_outlined), + title: const Text('Trim to Common Track End'), + subtitle: const Text( + 'End the clip where both tracks still have content', + ), + ), ListTile( onTap: _customAudioReplace, leading: const Icon(Icons.library_music_outlined), diff --git a/lib/core/models/video/video_render_data_model.dart b/lib/core/models/video/video_render_data_model.dart index d9963ed..4d174d3 100644 --- a/lib/core/models/video/video_render_data_model.dart +++ b/lib/core/models/video/video_render_data_model.dart @@ -30,6 +30,7 @@ class VideoRenderData { this.imageLayers, this.transform, this.enableAudio = true, + this.trimToCommonTrackEnd = false, this.startTime, this.endTime, this.colorFilters = const [], @@ -91,6 +92,7 @@ class VideoRenderData { List imageLayers = const [], ExportTransform? transform, bool enableAudio = true, + bool trimToCommonTrackEnd = false, Duration? startTime, Duration? endTime, double? blur, @@ -112,6 +114,7 @@ class VideoRenderData { imageLayers: imageLayers, transform: transform, enableAudio: enableAudio, + trimToCommonTrackEnd: trimToCommonTrackEnd, startTime: startTime, endTime: endTime, blur: blur, @@ -185,6 +188,27 @@ class VideoRenderData { /// **Default**: `true` final bool enableAudio; + /// Whether to end each clip where both of its tracks still have content. + /// + /// A source asset's audio and video tracks routinely end tens of + /// milliseconds apart, because capture and export stop them independently. + /// The export then spans the longer track and ends on missing audio or a + /// frozen frame — the seam a looping player replays every cycle. Enabling + /// this cuts each clip back to the earlier of the two ends. + /// + /// It shortens the export by the mismatch, so leave it off when a clip is + /// meant to outlast its own audio (stop motion held past a short sound). + /// A gap beyond 500ms is treated as content rather than a track-end + /// mismatch and is left alone. + /// + /// Honoured on Apple platforms only, and ignored when [composition] is set — + /// the layered path builds its own timeline, where shortening one clip would + /// shift every layer placed after it. Android decides its per-track output + /// lengths in `Media3 Transformer` and has no equivalent clamp. + /// + /// **Default**: `false` + final bool trimToCommonTrackEnd; + /// Optional start time for trimming the entire composition across all /// segments. final Duration? startTime; @@ -393,6 +417,7 @@ class VideoRenderData { 'colorFilters': colorFilterMaps, 'audioTracks': audioTrackMaps, 'enableAudio': enableAudio, + 'trimToCommonTrackEnd': trimToCommonTrackEnd, 'outputFormat': outputFormat.name, 'blur': blur, // Fall back to the quality config's bitrate when no explicit bitrate is @@ -422,6 +447,7 @@ class VideoRenderData { List? imageLayers, ExportTransform? transform, bool? enableAudio, + bool? trimToCommonTrackEnd, Duration? startTime, Duration? endTime, List? colorFilters, @@ -441,6 +467,7 @@ class VideoRenderData { imageLayers: imageLayers ?? this.imageLayers, transform: transform ?? this.transform, enableAudio: enableAudio ?? this.enableAudio, + trimToCommonTrackEnd: trimToCommonTrackEnd ?? this.trimToCommonTrackEnd, startTime: startTime ?? this.startTime, endTime: endTime ?? this.endTime, colorFilters: colorFilters ?? this.colorFilters, @@ -465,6 +492,7 @@ class VideoRenderData { 'imageLayers': imageLayers?.map((x) => x.toMap()).toList(), 'transform': transform?.toMap(), 'enableAudio': enableAudio, + 'trimToCommonTrackEnd': trimToCommonTrackEnd, 'startTime': startTime?.inMicroseconds, 'endTime': endTime?.inMicroseconds, 'colorFilters': colorFilters.map((x) => x.toMap()).toList(), @@ -509,6 +537,7 @@ class VideoRenderData { ? ExportTransform.fromMap(map['transform'] as Map) : null, enableAudio: map['enableAudio'] as bool, + trimToCommonTrackEnd: map['trimToCommonTrackEnd'] as bool? ?? false, startTime: map['startTime'] != null ? Duration(microseconds: safeParseInt(map['startTime'])) : null, @@ -550,6 +579,7 @@ class VideoRenderData { 'imageLayers: $imageLayers, ' 'transform: $transform, ' 'enableAudio: $enableAudio, ' + 'trimToCommonTrackEnd: $trimToCommonTrackEnd, ' 'startTime: $startTime, ' 'endTime: $endTime, ' 'colorFilters: $colorFilters, ' @@ -573,6 +603,7 @@ class VideoRenderData { listEquals(other.imageLayers, imageLayers) && other.transform == transform && other.enableAudio == enableAudio && + other.trimToCommonTrackEnd == trimToCommonTrackEnd && other.startTime == startTime && other.endTime == endTime && listEquals(other.colorFilters, colorFilters) && @@ -594,6 +625,7 @@ class VideoRenderData { imageLayers.hashCode ^ transform.hashCode ^ enableAudio.hashCode ^ + trimToCommonTrackEnd.hashCode ^ startTime.hashCode ^ endTime.hashCode ^ colorFilters.hashCode ^ diff --git a/pubspec.yaml b/pubspec.yaml index 82cd5cc..d0b18d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: pro_video_editor description: "A Flutter video editor: Seamlessly enhance your videos with user-friendly editing features." -version: 2.9.0 +version: 2.10.0 homepage: https://github.com/hm21/pro_video_editor/ repository: https://github.com/hm21/pro_video_editor/ documentation: https://github.com/hm21/pro_video_editor/ diff --git a/test/core/models/video/video_render_data_model_test.dart b/test/core/models/video/video_render_data_model_test.dart index 28c4f38..315d5ad 100644 --- a/test/core/models/video/video_render_data_model_test.dart +++ b/test/core/models/video/video_render_data_model_test.dart @@ -100,4 +100,63 @@ void main() { expect(map['bitrate'], 8000000); }); }); + + group('VideoRenderData trimToCommonTrackEnd', () { + VideoRenderData buildData({bool? trimToCommonTrackEnd}) { + return VideoRenderData( + id: 'test', + videoSegments: [VideoSegment(video: EditorVideo.file('test.mp4'))], + trimToCommonTrackEnd: trimToCommonTrackEnd ?? false, + ); + } + + test('defaults to off so exports keep their full length', () { + expect(buildData().trimToCommonTrackEnd, isFalse); + }); + + test('toMap serializes the value', () { + expect( + buildData(trimToCommonTrackEnd: true).toMap()['trimToCommonTrackEnd'], + isTrue, + ); + expect(buildData().toMap()['trimToCommonTrackEnd'], isFalse); + }); + + test('toAsyncMap sends the value over the platform channel', () async { + // toMap() is the Dart-side JSON form; toAsyncMap() is what actually + // reaches RenderConfig.fromArgs, so the native key is asserted here. + expect( + (await buildData( + trimToCommonTrackEnd: true, + ).toAsyncMap())['trimToCommonTrackEnd'], + isTrue, + ); + expect((await buildData().toAsyncMap())['trimToCommonTrackEnd'], isFalse); + }); + + test('toMap / fromMap roundtrip preserves the value', () { + final restored = VideoRenderData.fromMap( + buildData(trimToCommonTrackEnd: true).toMap(), + ); + expect(restored.trimToCommonTrackEnd, isTrue); + }); + + test('fromMap defaults to off for payloads without the key', () { + final map = buildData(trimToCommonTrackEnd: true).toMap() + ..remove('trimToCommonTrackEnd'); + + expect(VideoRenderData.fromMap(map).trimToCommonTrackEnd, isFalse); + }); + + test('copyWith overrides the value', () { + expect( + buildData().copyWith(trimToCommonTrackEnd: true).trimToCommonTrackEnd, + isTrue, + ); + expect( + buildData(trimToCommonTrackEnd: true).copyWith().trimToCommonTrackEnd, + isTrue, + ); + }); + }); } diff --git a/test/pro_video_editor_method_channel_test.mocks.dart b/test/pro_video_editor_method_channel_test.mocks.dart index 5d24ad0..9a3ce0b 100644 --- a/test/pro_video_editor_method_channel_test.mocks.dart +++ b/test/pro_video_editor_method_channel_test.mocks.dart @@ -344,6 +344,14 @@ class MockVideoRenderData extends _i1.Mock implements _i4.VideoRenderData { (super.noSuchMethod(Invocation.getter(#enableAudio), returnValue: false) as bool); + @override + bool get trimToCommonTrackEnd => + (super.noSuchMethod( + Invocation.getter(#trimToCommonTrackEnd), + returnValue: false, + ) + as bool); + @override List<_i4.ColorFilter> get colorFilters => (super.noSuchMethod( @@ -404,6 +412,7 @@ class MockVideoRenderData extends _i1.Mock implements _i4.VideoRenderData { List<_i4.ImageLayer>? imageLayers, _i4.ExportTransform? transform, bool? enableAudio, + bool? trimToCommonTrackEnd, Duration? startTime, Duration? endTime, List<_i4.ColorFilter>? colorFilters, @@ -424,6 +433,7 @@ class MockVideoRenderData extends _i1.Mock implements _i4.VideoRenderData { #imageLayers: imageLayers, #transform: transform, #enableAudio: enableAudio, + #trimToCommonTrackEnd: trimToCommonTrackEnd, #startTime: startTime, #endTime: endTime, #colorFilters: colorFilters, @@ -445,6 +455,7 @@ class MockVideoRenderData extends _i1.Mock implements _i4.VideoRenderData { #imageLayers: imageLayers, #transform: transform, #enableAudio: enableAudio, + #trimToCommonTrackEnd: trimToCommonTrackEnd, #startTime: startTime, #endTime: endTime, #colorFilters: colorFilters,