Files
CodenameEngine/source/funkin/game/cutscenes/VideoCutscene.hx
T
b447031e2d Enable Mac OS Universal support, update hxvlc & actions dependencies, fix VIDEO_CUTSCENES (#931)
* Update hxvlc to 2.2.6

* Remove `arch -x86_64` as no longer needed

Since `cne-lime` got rebased with FunkinCrew's, which includes Apple Silicon (ARM64) support.

* Wtf

* Rename **Mac OS x64** to **Mac OS Universal** in the ReadME

Reason: **x64** is only powered by Intel/AMD, not ARM, so it will make the macOS users to think macOS builds still uses Intel.

* Enable videos for iOS

Since hxvlc 2.0.0 or newer, it fixed the video playback for iOS.

* Fixed the extension-androidtools import, when using extension-androidtools 2.2.X.

* Replace `start` with `wait` in `VideoCutscene.hx`

* Revert

* Nvm I think I found out why I guess

* Will this fix some actions deprecations?

* Oops

* F#CK

* I'm gonna lose it

* I forgot again

I'm going to scream into the pillow, If I keep forgetting to update those.

* Update macOS version to 26 in the macOS workflow

* Remove old hxvlc apis from the `VideoCutscene.hx` class, since hxvlc got updated to the latest

* Fix the f#cking conflicts

* Now it works

---------

Co-authored-by: Ralty <78720179+Raltyro@users.noreply.github.com>
2026-03-29 13:41:27 +07:00

242 lines
6.6 KiB
Haxe

package funkin.game.cutscenes;
import flixel.tweens.FlxTween;
import flixel.addons.display.FlxBackdrop;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.backend.FunkinText;
import haxe.Int64;
import haxe.io.FPHelper;
import haxe.io.Path;
import haxe.xml.Access;
#if sys
import sys.io.File;
#end
#if VIDEO_CUTSCENES
import hxvlc.flixel.FlxVideoSprite;
#end
/**
* Substate made for video cutscenes. To use it in a scripted cutscene, call `startVideo`.
*/
class VideoCutscene extends Cutscene {
private static var curVideo:Int = 0; // internal for zip videos
var path:String;
var localPath:String;
#if VIDEO_CUTSCENES
var video:FlxVideoSprite;
final mutex = new sys.thread.Mutex();
var cutsceneCamera:FlxCamera;
var text:FunkinText;
var loadingBackdrop:FlxBackdrop;
private var __loaded:Bool = false;
var bg:FlxSprite;
var subtitle:FunkinText;
public var subtitles:Array<CutsceneSubtitle> = [];
var curSubtitle:Int = 0;
#end
public function new(path:String, callback:Void->Void) {
super(callback, false);
localPath = Assets.getPath(this.path = path);
}
public override function create() {
super.create();
// TODO: get vlc to stop autoloading those goddamn subtitles (use different file ext??)
#if VIDEO_CUTSCENES
cutsceneCamera = new FlxCamera();
cutsceneCamera.bgColor = 0;
FlxG.cameras.add(cutsceneCamera, false);
parseSubtitles();
add(video = new FlxVideoSprite());
video.antialiasing = true;
video.bitmap.onEndReached.add(close);
video.bitmap.onFormatSetup.add(function() if (video.bitmap != null && video.bitmap.bitmapData != null) {
final width = video.bitmap.bitmapData.width;
final height = video.bitmap.bitmapData.height;
final scale:Float = Math.min(FlxG.width / width, FlxG.height / height);
video.setGraphicSize(width * scale, height * scale);
video.updateHitbox();
video.screenCenter();
});
//cover = new FlxSprite(0, FlxG.height * 0.85).makeSolid(FlxG.width + 50, FlxG.height + 50, 0xFF000000);
//cover.scrollFactor.set(0, 0);
//cover.screenCenter();
//add(cover);
bg = new FlxSprite(0, FlxG.height * 0.85).makeGraphic(1, 1, 0xFF000000);
bg.alpha = 0.5;
bg.visible = false;
subtitle = new FunkinText(0, FlxG.height * 0.875, 0, "", 20);
subtitle.alignment = CENTER;
subtitle.visible = false;
text = new FunkinText(10, 10, Std.int(FlxG.width / 2), "Loading video...");
text.cameras = [cutsceneCamera];
text.visible = false;
@:privateAccess
text.regenGraphic();
add(text);
loadingBackdrop = new FlxBackdrop(text.graphic, X);
loadingBackdrop.y = FlxG.height - 20 - loadingBackdrop.height;
loadingBackdrop.velocity.x = 70;
loadingBackdrop.cameras = [cutsceneCamera];
add(loadingBackdrop);
loadingBackdrop.alpha = 0;
FlxTween.tween(loadingBackdrop, {alpha: 1}, 0.5, {ease: FlxEase.sineInOut});
Main.execAsync(function() {
if (video.load(localPath)) FlxTimer.wait(0.001, function() {
mutex.acquire(); onReady(); mutex.release();
});
else { mutex.acquire(); close(); mutex.release(); }
});
add(bg);
add(subtitle);
cameras = [cutsceneCamera];
#else
close();
#end
}
#if VIDEO_CUTSCENES
public function parseSubtitles() {
var subtitlesPath = '${Path.withoutExtension(path)}.srt';
if (Assets.exists(subtitlesPath)) {
var text = Assets.getText('$subtitlesPath').split("\n");
while(text.length > 0) {
var head = text.shift();
if (head == null || head.trim() == "")
continue; // no head (EOF or empty line), skipping
if (Std.parseInt(head) == null)
continue; // invalid index, skipping
var id = head;
var time = text.shift();
if (time == null) continue; // no time (EOF), skipping
var arrowIndex = time.indexOf('-->');
if (arrowIndex < 0) continue; // no -->, skipping
var beginTime = splitTime(time.substr(0, arrowIndex).trim());
var endTime = splitTime(time.substr(arrowIndex + 3).trim());
if (beginTime < 0 || endTime < 0) continue; // invalid timestamps
var subtitleText:Array<String> = [];
var t:String = text.shift();
while(t != null && t.trim() != "") {
subtitleText.push(t);
t = text.shift();
}
if (subtitleText.length <= 0) continue; // empty subtitle, skipping
var lastSub = subtitles.last();
if (lastSub != null && lastSub.subtitle == "" && lastSub.time >= beginTime)
subtitles.pop(); // remove last subtitle auto reset to prevent bugs
subtitles.push({
subtitle: subtitleText.join(""),
time: beginTime * 1000
});
subtitles.push({
subtitle: "",
time: endTime * 1000
});
}
}
}
public static function splitTime(str:String):Float {
if (str == null || str.trim() == "") return -1;
var multipliers:Array<Float> = [1, 60, 3600, 86400, 31536000]; // no way a cutscene will last longer than years
var timeSplit:Array<Null<Float>> = [for(e in str.split(":")) Std.parseFloat(e.replace(",", "."))];
var time:Float = 0;
for(k=>i in timeSplit) {
var mul = multipliers[timeSplit.length - 1 - k];
if (i != null)
time += i * mul;
}
return time;
}
public inline function onReady() {
trace("VideoCutscene: Ready");
FlxTween.cancelTweensOf(loadingBackdrop);
FlxTween.tween(loadingBackdrop, {alpha: 0}, 0.7, {ease: FlxEase.sineInOut, onComplete: function(_) {
loadingBackdrop.destroy();
text.destroy();
}});
pausable = __loaded = true;
video.play();
}
public override function update(elapsed:Float) {
super.update(elapsed);
if(loadingBackdrop != null) return;
@:privateAccess
var time:Int64 = video.bitmap.time;
var time:Float = FPHelper.i64ToDouble(time.low, time.high);
if(curSubtitle < subtitles.length && subtitles[curSubtitle].time < time) {
setSubtitle(subtitles[curSubtitle]);
curSubtitle++;
}
}
public override function pauseCutscene() {
video.pause();
super.pauseCutscene();
}
public override function onResumeCutscene(event) {
video.resume();
super.onResumeCutscene(event);
}
public override function onRestartCutscene(event) {
closeSubState();
video.bitmap.position = curSubtitle = 0;
setSubtitle({time: -1, subtitle: ""});
video.resume();
}
public function setSubtitle(sub:CutsceneSubtitle) {
if (bg.visible = subtitle.visible = (sub.subtitle.length > 0)) {
subtitle.text = sub.subtitle;
subtitle.screenCenter(X);
bg.scale.set(subtitle.width + 8, subtitle.height + 8);
bg.updateHitbox();
bg.setPosition(subtitle.x - 4, subtitle.y - 4);
}
}
public override function destroy() {
FlxG.cameras.remove(cutsceneCamera, true);
video.destroy();
super.destroy();
}
#end
}
typedef CutsceneSubtitle = {
var time:Float; // time in ms
var subtitle:String; // subtitle text
}