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
2 changes: 1 addition & 1 deletion editor/backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<groupId>com.engine</groupId>
<artifactId>editor-backend</artifactId>
<version>0.4.0-RELEASE</version>
<version>0.5.0-RELEASE</version>
<name>editor-backend</name>
<description>Arvexis - Editor Backend</description>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;

/**
* Manage the project's custom.css file for runtime UI customization.
* Manage the project's custom CSS files for runtime UI customization.
* Supports three files: custom.css (general), buttons.css, subtitles.css.
*/
@RestController
@RequestMapping("/api/custom-css")
public class CustomCssController {

private static final Set<String> ALLOWED_FILES = Set.of("custom", "buttons", "subtitles");

private final ProjectService projectService;

public CustomCssController(ProjectService projectService) {
Expand All @@ -26,24 +30,45 @@ public CustomCssController(ProjectService projectService) {

@GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<String> getCustomCss() throws IOException {
Path cssFile = cssPath();
return readCssFile("custom");
}

@PutMapping(consumes = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<Map<String, Object>> saveCustomCss(@RequestBody String content) throws IOException {
return writeCssFile("custom", content);
}

@GetMapping(value = "/{name}", produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<String> getNamedCss(@PathVariable String name) throws IOException {
if (!ALLOWED_FILES.contains(name)) return ResponseEntity.notFound().build();
return readCssFile(name);
}

@PutMapping(value = "/{name}", consumes = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<Map<String, Object>> saveNamedCss(@PathVariable String name,
@RequestBody String content) throws IOException {
if (!ALLOWED_FILES.contains(name)) return ResponseEntity.notFound().build();
return writeCssFile(name, content);
}

private ResponseEntity<String> readCssFile(String name) throws IOException {
Path cssFile = cssPath(name);
if (Files.exists(cssFile)) {
return ResponseEntity.ok(Files.readString(cssFile, StandardCharsets.UTF_8));
}
return ResponseEntity.ok("");
}

@PutMapping(consumes = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<Map<String, Object>> saveCustomCss(@RequestBody String content) throws IOException {
Path cssFile = cssPath();
private ResponseEntity<Map<String, Object>> writeCssFile(String name, String content) throws IOException {
Path cssFile = cssPath(name);
Files.writeString(cssFile, content, StandardCharsets.UTF_8);
return ResponseEntity.ok(Map.of(
"message", "custom.css saved",
"message", name + ".css saved",
"path", cssFile.toAbsolutePath().toString()
));
}

private Path cssPath() {
return projectService.getCurrentProjectPath().resolve("custom.css");
private Path cssPath(String name) {
return projectService.getCurrentProjectPath().resolve(name + ".css");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,12 @@ public ResponseEntity<TransitionResponse> saveAudio(
) {
return ResponseEntity.ok(transitionService.saveAudioTracks(id, tracks));
}

@PutMapping("/background-color")
public ResponseEntity<TransitionResponse> setBackgroundColor(
@PathVariable String id,
@RequestBody Map<String, String> body
) {
return ResponseEntity.ok(transitionService.setBackgroundColor(id, body.get("backgroundColor")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public record UpdateNodeRequest(
String name,
Boolean isEnd,
Boolean autoContinue,
Boolean loopVideo,
String backgroundColor,
String decisionAppearanceConfig,
String musicAssetId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,33 @@ public void generateHls(Path inputPath, Path outputDir, HlsOptions options) thro
if (!result.isSuccess()) {
throw new IOException("FFmpeg HLS generation failed: " + result.getStderr());
}
// FFmpeg writes TARGETDURATION as floor(max_segment_duration), which gives 0 for
// clips shorter than 1 second. Fix it to ceil(max_extinf) per HLS spec (RFC 8216 §4.3.3.1).
fixTargetDuration(outputDir.resolve(options.getPlaylistName()));
}

private static void fixTargetDuration(Path playlist) {
if (!Files.exists(playlist)) return;
try {
String content = Files.readString(playlist);
// Find the maximum EXTINF duration value
double maxExtinf = 0;
for (String line : content.split("\n")) {
line = line.trim();
if (line.startsWith("#EXTINF:")) {
try {
double d = Double.parseDouble(line.substring(8).replace(",", "").trim());
if (d > maxExtinf) maxExtinf = d;
} catch (NumberFormatException ignored) {}
}
}
int correct = (int) Math.ceil(maxExtinf);
if (correct < 1) correct = 1;
String fixed = content.replaceFirst(
"#EXT-X-TARGETDURATION:\\d+",
"#EXT-X-TARGETDURATION:" + correct);
if (!fixed.equals(content)) Files.writeString(playlist, fixed);
} catch (IOException ignored) {}
}

@Override
Expand Down Expand Up @@ -173,7 +200,7 @@ private List<String> buildCompositeCommand(CompositeSpec spec) {
List<VideoLayerSpec> layers = spec.getVideoLayers();
for (VideoLayerSpec layer : layers) {
List<String> preOpts = new ArrayList<>();
if (layer.isHasAlpha()) {
if (layer.isHasAlpha() && "vp9".equalsIgnoreCase(layer.getCodec())) {
preOpts.add("-vcodec");
preOpts.add("libvpx-vp9");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,27 @@ public class VideoLayerSpec {
private final double startAt;
private final int order;
private final boolean hasAlpha;
private final String codec;

private final boolean freezeLastFrame;

public VideoLayerSpec(Path filePath, double startAt, int order, boolean hasAlpha, boolean freezeLastFrame) {
this(filePath, startAt, order, hasAlpha, freezeLastFrame, null);
}

public VideoLayerSpec(Path filePath, double startAt, int order, boolean hasAlpha, boolean freezeLastFrame, String codec) {
this.filePath = filePath;
this.startAt = startAt;
this.order = order;
this.hasAlpha = hasAlpha;
this.freezeLastFrame = freezeLastFrame;
this.codec = codec;
}

public Path getFilePath() { return filePath; }
public double getStartAt() { return startAt; }
public int getOrder() { return order; }
public boolean isHasAlpha() { return hasAlpha; }
public boolean isFreezeLastFrame() { return freezeLastFrame; }
public String getCodec() { return codec; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public record NodeExit(String key, String label, boolean isDefault) {}
private boolean isRoot;
private boolean isEnd;
private boolean autoContinue;
private boolean loopVideo;
private String backgroundColor;
private String decisionAppearanceConfig;
private String musicAssetId;
Expand Down Expand Up @@ -44,6 +45,10 @@ public GraphNode() {}
public boolean isAutoContinue() { return autoContinue; }
public void setAutoContinue(boolean autoContinue) { this.autoContinue = autoContinue; }

@JsonProperty("loopVideo")
public boolean isLoopVideo() { return loopVideo; }
public void setLoopVideo(boolean loopVideo) { this.loopVideo = loopVideo; }

public String getBackgroundColor() { return backgroundColor; }
public void setBackgroundColor(String backgroundColor) { this.backgroundColor = backgroundColor; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,12 @@ private Path buildPackage(PreviewJob job, Path projectDir, Path outputBase,
buildReadme(projectName),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

// custom.css (user-editable runtime styles)
Path customCss = projectDir.resolve("custom.css");
if (Files.exists(customCss)) {
Files.copy(customCss, distDir.resolve("custom.css"), StandardCopyOption.REPLACE_EXISTING);
// User-editable runtime CSS files (buttons, subtitles, general)
for (String cssName : List.of("buttons.css", "subtitles.css", "custom.css")) {
Path cssFile = projectDir.resolve(cssName);
if (Files.exists(cssFile)) {
Files.copy(cssFile, distDir.resolve(cssName), StandardCopyOption.REPLACE_EXISTING);
}
}

// Create dist.zip
Expand Down Expand Up @@ -318,7 +320,7 @@ private void createZip(Path distDir, Path outputBase, Path assetsDir, Path zipFi
Files.newOutputStream(zipFile,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {

// Add dist/ contents (manifest, runtime.jar, scripts, README, custom.css)
// Add dist/ contents (manifest, runtime.jar, scripts, README, CSS files)
try (var walk = Files.walk(distDir)) {
walk.filter(Files::isRegularFile).forEach(p -> {
String name = "game/" + distDir.relativize(p).toString();
Expand Down Expand Up @@ -389,7 +391,8 @@ private void compileScene(Map<String, Object> node, ProjectConfigData config,
if (filePath == null) continue;
boolean hasAlpha = r.get("hasAlpha") instanceof Boolean b ? b : Boolean.TRUE.equals(r.get("hasAlpha"));
boolean freeze = r.get("freezeLastFrame") instanceof Boolean b ? b : Boolean.TRUE.equals(r.get("freezeLastFrame"));
layers.add(new VideoLayerSpec(Path.of(filePath), toDouble(r.get("startAt")), i, hasAlpha, freeze));
String codec = r.get("codec") instanceof String s ? s : null;
layers.add(new VideoLayerSpec(Path.of(filePath), toDouble(r.get("startAt")), i, hasAlpha, freeze, codec));
}

List<AudioTrackSpec> tracks = new ArrayList<>();
Expand Down Expand Up @@ -491,7 +494,8 @@ private void compileVideoTransition(Map<String, Object> trans, ProjectConfigData
if (fp == null) continue;
boolean hasAlpha = r.get("hasAlpha") instanceof Boolean b ? b : Boolean.TRUE.equals(r.get("hasAlpha"));
boolean freeze = r.get("freezeLastFrame") instanceof Boolean b ? b : Boolean.TRUE.equals(r.get("freezeLastFrame"));
layers.add(new VideoLayerSpec(Path.of(fp), toDouble(r.get("startAt")), i, hasAlpha, freeze));
String codec = r.get("codec") instanceof String s ? s : null;
layers.add(new VideoLayerSpec(Path.of(fp), toDouble(r.get("startAt")), i, hasAlpha, freeze, codec));
}
List<AudioTrackSpec> tracks = new ArrayList<>();
for (int i = 0; i < audioData.size(); i++) {
Expand All @@ -503,7 +507,10 @@ private void compileVideoTransition(Map<String, Object> trans, ProjectConfigData
double duration = toDouble(trans.get("duration"));
if (duration <= 0) duration = 2.0;

String bgHex = config.getDefaultBackgroundColor() != null ? config.getDefaultBackgroundColor() : "#000000";
// Prefer per-transition backgroundColor; fall back to project default, then white
String bgHex = trans.get("backgroundColor") instanceof String s && !s.isBlank() ? s
: config.getDefaultBackgroundColor() != null ? config.getDefaultBackgroundColor()
: "#ffffff";
String ffmpegBg = bgHex.replaceFirst("^#", "0x");

CompositeSpec spec = CompositeSpec.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public GraphNode updateNode(String id, UpdateNodeRequest req) {
String name = req.name() != null ? req.name().trim() : existing.getName();
boolean isEnd = req.isEnd() != null ? req.isEnd() : existing.isEnd();
boolean autoCont = req.autoContinue() != null ? req.autoContinue() : existing.isAutoContinue();
boolean loopVid = req.loopVideo() != null ? req.loopVideo() : existing.isLoopVideo();
String bgColor = req.backgroundColor() != null ? req.backgroundColor() : existing.getBackgroundColor();
String dac = req.decisionAppearanceConfig() != null
? req.decisionAppearanceConfig()
Expand All @@ -96,10 +97,10 @@ public GraphNode updateNode(String id, UpdateNodeRequest req) {
if (name.isBlank()) throw new ProjectException("Node name must not be blank");

jdbc.update("""
UPDATE nodes SET name=?, is_end=?, auto_continue=?, background_color=?,
UPDATE nodes SET name=?, is_end=?, auto_continue=?, loop_video=?, background_color=?,
decision_appearance_config=?, music_asset_id=?, pos_x=?, pos_y=?
WHERE id=?
""", name, isEnd ? 1 : 0, autoCont ? 1 : 0, bgColor, dac, musicAsset, posX, posY, id);
""", name, isEnd ? 1 : 0, autoCont ? 1 : 0, loopVid ? 1 : 0, bgColor, dac, musicAsset, posX, posY, id);

return getNode(id);
}
Expand Down Expand Up @@ -228,6 +229,7 @@ private GraphNode mapNode(ResultSet rs, int row) throws SQLException {
n.setRoot(rs.getInt("is_root") == 1);
n.setEnd(rs.getInt("is_end") == 1);
n.setAutoContinue(rs.getInt("auto_continue") == 1);
n.setLoopVideo(rs.getInt("loop_video") == 1);
n.setBackgroundColor(rs.getString("background_color"));
n.setDecisionAppearanceConfig(rs.getString("decision_appearance_config"));
n.setMusicAssetId(rs.getString("music_asset_id"));
Expand Down
Loading
Loading