Add basic edit transitions
This commit is contained in:
parent
c3c6c7a790
commit
e2037a7629
|
|
@ -801,7 +801,7 @@ It writes the script only and requires an imported audio file.
|
||||||
18. [x] Add concat-based final timeline rendering.
|
18. [x] Add concat-based final timeline rendering.
|
||||||
19. [x] Add music and voiceover audio mixing.
|
19. [x] Add music and voiceover audio mixing.
|
||||||
20. [x] Add render manifest with command metadata and output details.
|
20. [x] Add render manifest with command metadata and output details.
|
||||||
21. [ ] Add basic transition support: cut, crossfade, fade in, fade out.
|
21. [x] Add basic transition support: cut, crossfade, fade in, fade out.
|
||||||
22. [ ] Add SFX cue support.
|
22. [ ] Add SFX cue support.
|
||||||
23. [ ] Add optional generated voiceover adapter interface.
|
23. [ ] Add optional generated voiceover adapter interface.
|
||||||
24. [ ] Add structured logs and metrics for analysis, planning, and rendering durations.
|
24. [ ] Add structured logs and metrics for analysis, planning, and rendering durations.
|
||||||
|
|
|
||||||
|
|
@ -84,15 +84,26 @@ public class FfmpegEditRenderer implements EditRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> segmentCommand(String source, EditDecision decision, Path output) {
|
List<String> segmentCommand(String source, EditDecision decision, Path output) {
|
||||||
String filter = "scale=%d:%d:force_original_aspect_ratio=decrease,".formatted(
|
double outputDuration = (decision.sourceEndSeconds() - decision.sourceStartSeconds()) / decision.playbackSpeed();
|
||||||
|
StringBuilder filter = new StringBuilder("scale=%d:%d:force_original_aspect_ratio=decrease,".formatted(
|
||||||
properties.getOutputWidth(), properties.getOutputHeight())
|
properties.getOutputWidth(), properties.getOutputHeight())
|
||||||
+ "pad=%d:%d:(ow-iw)/2:(oh-ih)/2,format=yuv420p".formatted(
|
+ "pad=%d:%d:(ow-iw)/2:(oh-ih)/2,format=yuv420p".formatted(
|
||||||
properties.getOutputWidth(), properties.getOutputHeight());
|
properties.getOutputWidth(), properties.getOutputHeight()));
|
||||||
|
filter.append(",setpts=PTS/").append(decision.playbackSpeed());
|
||||||
|
double fadeDuration = Math.min(0.5, outputDuration / 2);
|
||||||
|
if ("fade-in".equals(decision.transitionIn()) || "crossfade".equals(decision.transitionIn())) {
|
||||||
|
filter.append(",fade=t=in:st=0:d=").append(fadeDuration);
|
||||||
|
}
|
||||||
|
if ("fade-out".equals(decision.transitionOut()) || "crossfade".equals(decision.transitionOut())) {
|
||||||
|
filter.append(",fade=t=out:st=").append(Math.max(0, outputDuration - fadeDuration))
|
||||||
|
.append(":d=").append(fadeDuration);
|
||||||
|
}
|
||||||
List<String> command = new ArrayList<>(List.of(
|
List<String> command = new ArrayList<>(List.of(
|
||||||
properties.getFfmpegBinary(), "-hide_banner", "-y",
|
properties.getFfmpegBinary(), "-hide_banner", "-y",
|
||||||
"-ss", Double.toString(decision.sourceStartSeconds()),
|
"-ss", Double.toString(decision.sourceStartSeconds()),
|
||||||
"-to", Double.toString(decision.sourceEndSeconds()),
|
"-to", Double.toString(decision.sourceEndSeconds()),
|
||||||
"-i", source, "-vf", filter,
|
"-i", source, "-vf", filter.toString(),
|
||||||
|
"-af", "atempo=" + decision.playbackSpeed(),
|
||||||
"-r", Integer.toString(properties.getOutputFrameRate()),
|
"-r", Integer.toString(properties.getOutputFrameRate()),
|
||||||
"-map", "0:v:0", "-map", "0:a?",
|
"-map", "0:v:0", "-map", "0:a?",
|
||||||
"-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
|
"-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ class FfmpegEditRendererTest {
|
||||||
var command = renderer.segmentCommand("source.mp4", decision, Path.of("segment.mp4"));
|
var command = renderer.segmentCommand("source.mp4", decision, Path.of("segment.mp4"));
|
||||||
|
|
||||||
assertThat(command).containsSubsequence("-ss", "1.25", "-to", "5.5", "-i", "source.mp4");
|
assertThat(command).containsSubsequence("-ss", "1.25", "-to", "5.5", "-i", "source.mp4");
|
||||||
assertThat(command).contains("scale=1920:1080:force_original_aspect_ratio=decrease,"
|
assertThat(command).anyMatch(argument -> argument.startsWith(
|
||||||
+ "pad=1920:1080:(ow-iw)/2:(oh-ih)/2,format=yuv420p");
|
"scale=1920:1080:force_original_aspect_ratio=decrease,"
|
||||||
|
+ "pad=1920:1080:(ow-iw)/2:(oh-ih)/2,format=yuv420p"));
|
||||||
assertThat(command).containsSubsequence("-c:v", "libx264", "-c:a", "aac");
|
assertThat(command).containsSubsequence("-c:v", "libx264", "-c:a", "aac");
|
||||||
assertThat(command).endsWith("segment.mp4");
|
assertThat(command).endsWith("segment.mp4");
|
||||||
}
|
}
|
||||||
|
|
@ -53,4 +54,20 @@ class FfmpegEditRendererTest {
|
||||||
+ "[0:a][music][voice]amix=inputs=3:duration=first:dropout_transition=2[a]");
|
+ "[0:a][music][voice]amix=inputs=3:duration=first:dropout_transition=2[a]");
|
||||||
assertThat(command).containsSubsequence("-c:v", "copy", "-c:a", "aac");
|
assertThat(command).containsSubsequence("-c:v", "copy", "-c:a", "aac");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appliesFadeAndPlaybackTreatmentsToSegments() {
|
||||||
|
VideoClippingProperties properties = new VideoClippingProperties();
|
||||||
|
FfmpegEditRenderer renderer = new FfmpegEditRenderer(properties, mock(EditProjectStore.class),
|
||||||
|
mock(EditProjectService.class), mock(EditPlanValidator.class), command -> null);
|
||||||
|
EditDecision decision = new EditDecision("clip-1", 0, 4, 0, 2,
|
||||||
|
"fade-in", "crossfade", 2, "grade", "reason");
|
||||||
|
|
||||||
|
var command = renderer.segmentCommand("source.mp4", decision, Path.of("segment.mp4"));
|
||||||
|
|
||||||
|
assertThat(command).contains("scale=1920:1080:force_original_aspect_ratio=decrease,"
|
||||||
|
+ "pad=1920:1080:(ow-iw)/2:(oh-ih)/2,format=yuv420p,setpts=PTS/2.0,"
|
||||||
|
+ "fade=t=in:st=0:d=0.5,fade=t=out:st=1.5:d=0.5");
|
||||||
|
assertThat(command).containsSubsequence("-af", "atempo=2.0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue