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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 1.16.2
- **FIX**(android, iOS, macOS): Eliminate audible clicks/gaps at custom audio loop boundaries and clip transitions. Each custom audio track is now pre-rendered to a single gap-less PCM WAV file before being inserted into the composition, avoiding AAC encoder frame realignment (Android Media3) and codec priming/padding artifacts (AVFoundation `insertTimeRange` per loop iteration on iOS/macOS). Volume control remains in the mixer layer for post-hoc adjustments without re-decoding.

## 1.16.1
- **FIX**(iOS, macOS): Fix crash `No source tracks available for compositing` when rendering H.264 MP4 files whose container duration slightly exceeds the video track's actual frame duration. The compositor time range is now clamped to the video track's real `timeRange` before inserting into the composition, preventing AVFoundation from requesting frames that have no pixel buffer. A black-frame fallback was also added as a safety net.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,15 +310,35 @@ class RenderVideo(private val context: Context) {
// Create composition (now fast - no manual audio mixing needed, Media3 handles it natively)
Thread {
try {
val composition = applyComposition(
val compositionResult = applyComposition(
context = context,
config = config,
videoEffects = videoEffects,
audioEffects = audioEffects
)

mainHandler.post {
if (composition != null) {
if (compositionResult != null) {
val composition = compositionResult.composition
val audioTempFiles = compositionResult.temporaryFiles

transformer.addListener(object : Transformer.Listener {
override fun onCompleted(
composition: Composition,
result: ExportResult
) {
cleanupAudioTempFiles(audioTempFiles)
}

override fun onError(
composition: Composition,
result: ExportResult,
exception: ExportException
) {
cleanupAudioTempFiles(audioTempFiles)
}
})

transformer.start(composition, outputFile.absolutePath)

// Start progress tracking loop
Expand Down Expand Up @@ -349,4 +369,27 @@ class RenderVideo(private val context: Context) {
}
}.start()
}

/**
* Deletes pre-rendered audio temp files (typically WAVs from
* AudioPreRenderer) created while building the composition.
*/
private fun cleanupAudioTempFiles(files: List<File>) {
for (file in files) {
try {
if (file.exists()) {
val deleted = file.delete()
Log.d(
RENDER_TAG,
"Cleanup pre-rendered audio file: ${file.name}, deleted=$deleted"
)
}
} catch (e: Exception) {
Log.w(
RENDER_TAG,
"Failed to delete pre-rendered audio file ${file.name}: ${e.message}"
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import androidx.media3.common.audio.AudioProcessor
import androidx.media3.common.util.UnstableApi
import androidx.media3.transformer.Composition
import ch.waio.pro_video_editor.src.features.render.models.RenderConfig
import java.io.File

/**
* Result of building a composition: the composition itself plus a list of
* temporary files that were created during the build (e.g. pre-rendered
* audio WAVs). The caller MUST delete these temporary files after the
* Transformer export finishes (success or failure).
*/
data class CompositionResult(
val composition: Composition,
val temporaryFiles: List<File>
)

/**
* Creates a Media3 Composition from render configuration.
Expand All @@ -18,17 +30,19 @@ import ch.waio.pro_video_editor.src.features.render.models.RenderConfig
* @param config The render configuration containing all composition parameters
* @param videoEffects List of video effects to apply (from EffectsProcessor)
* @param audioEffects List of audio effects to apply (from EffectsProcessor)
* @return Composition ready for transformer, or null if no video clips provided
* @return [CompositionResult] with the composition and any temporary files
* created during the build, or null if no video clips were provided.
*/
@UnstableApi
fun applyComposition(
context: Context,
config: RenderConfig,
videoEffects: List<Effect>,
audioEffects: List<AudioProcessor>
): Composition? {
return CompositionBuilder(context, config)
): CompositionResult? {
val builder = CompositionBuilder(context, config)
.setVideoEffects(videoEffects)
.setAudioEffects(audioEffects)
.build()
val composition = builder.build() ?: return null
return CompositionResult(composition, builder.temporaryFiles.toList())
}
Loading
Loading