Done now rather than later: the application ID is permanent once published -- Play treats a change as an entirely different app -- so this is the last cheap moment to choose it. applicationId / namespace dev.jasonmross.mediaconverter -> org.libremediaconverter source tree java/dev/jasonmross/mediaconverter -> java/org/libremediaconverter gradle project AndroidMediaConverter -> LibreMediaConverter theme Theme.MediaConverter -> Theme.LibreMediaConverter compose theme MediaConverterTheme -> LibreMediaConverterTheme display name "Media Converter" -> "LibreMediaConverter" org.* rather than dev.jasonmross.* because "Libre" signals a project rather than a personal app, and a project-owned namespace lets maintainership move later without the identifier contradicting reality. The source trees moved with git mv so history follows the files instead of showing 42 deletions beside 42 additions. Verified after the rename: 66 unit tests, and 40 instrumented tests on an API 36 emulator, 0 failures. The built APK reports org.libremediaconverter, and no stale jasonmross, AndroidMediaConverter or MediaConverterTheme identifiers remain anywhere in the tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
84 lines
2.4 KiB
Kotlin
84 lines
2.4 KiB
Kotlin
package org.libremediaconverter.model
|
|
|
|
import org.junit.Assert.assertEquals
|
|
import org.junit.Test
|
|
|
|
/**
|
|
* The stream-copy-vs-re-encode decision.
|
|
*
|
|
* This matters more than it looks: the `concat` demuxer does not reject mismatched
|
|
* inputs loudly, it can emit a file whose later segments are garbled. So the default
|
|
* on any doubt has to be re-encoding.
|
|
*/
|
|
class ConcatPlannerTest {
|
|
|
|
private fun clip(
|
|
video: String? = "h264",
|
|
audio: String? = "aac",
|
|
width: Int = 1920,
|
|
height: Int = 1080,
|
|
fps: Int = 30,
|
|
) = ConcatInput(video, audio, width, height, fps)
|
|
|
|
@Test
|
|
fun `identical clips can be stream copied`() {
|
|
assertEquals(
|
|
ConcatStrategy.STREAM_COPY,
|
|
ConcatPlanner.plan(listOf(clip(), clip())),
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `a single input needs no re-encode`() {
|
|
assertEquals(ConcatStrategy.STREAM_COPY, ConcatPlanner.plan(listOf(clip())))
|
|
}
|
|
|
|
@Test
|
|
fun `different video codecs force a re-encode`() {
|
|
assertEquals(
|
|
ConcatStrategy.REENCODE,
|
|
ConcatPlanner.plan(listOf(clip(video = "h264"), clip(video = "hevc"))),
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `different audio codecs force a re-encode`() {
|
|
assertEquals(
|
|
ConcatStrategy.REENCODE,
|
|
ConcatPlanner.plan(listOf(clip(audio = "aac"), clip(audio = "opus"))),
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `different resolutions force a re-encode`() {
|
|
assertEquals(
|
|
ConcatStrategy.REENCODE,
|
|
ConcatPlanner.plan(listOf(clip(width = 1920, height = 1080), clip(width = 1280, height = 720))),
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `different frame rates force a re-encode`() {
|
|
assertEquals(
|
|
ConcatStrategy.REENCODE,
|
|
ConcatPlanner.plan(listOf(clip(fps = 30), clip(fps = 60))),
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `an unknown codec is not treated as a match`() {
|
|
// Two nulls are not evidence of agreement. Assuming they match is exactly how
|
|
// a silent corrupt concat happens.
|
|
assertEquals(
|
|
ConcatStrategy.REENCODE,
|
|
ConcatPlanner.plan(listOf(clip(video = null), clip(video = null))),
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `a mismatch anywhere in a longer list is caught`() {
|
|
val clips = listOf(clip(), clip(), clip(), clip(width = 640, height = 480), clip())
|
|
assertEquals(ConcatStrategy.REENCODE, ConcatPlanner.plan(clips))
|
|
}
|
|
}
|