reckfellENGINE

Video: playback, pause and seeking

A complete C# UI, Theora OGV, aspect ratio, looping and chroma key.

On this page

Supported input

This build's built-in decoder reads Theora in an Ogg container, using .ogv. An extension does not convert a codec; renaming MP4 to OGV will not help. Other formats need a separate VideoStream implementation or extension. Built-in MP4, H.264 or WebM support is not supplied by this example.

Download the teaching OGV for Reckfell 1.0.0. It is original footage from the frame-recording example: 512 × 288 at 30 FPS for 4 seconds. No external footage or encoder installation is required to try it.

Run the interface

Create a Control level root. Save the clip as Clips/recording-slate.ogv, add VideoDemo.cs, attach it to the root, build and run with F6. Press Play. A blue square moves across the screen; Pause retains the position, Resume continues and Stop stops playback. Pressing Play again starts from the beginning.

VideoDemo.cs
C#
using System;
using Reckfell;

public partial class VideoDemo : Control
{
    [Export(PropertyHint.File,"*.ogv")] public string VideoPath { get; set; } = "asset://Clips/recording-slate.ogv";
    public VideoStreamPlayer Video { get; private set; } = null!;
    public Label Status { get; private set; } = null!;
    public Button PauseButton { get; private set; } = null!;
    public int FinishedCount { get; private set; }
    private bool _russian;
    private string Text(string ru,string en) => _russian ? ru : en;

    public override void _Ready()
    {
        _russian=TranslationServer.GetLocale().StartsWith("ru",StringComparison.OrdinalIgnoreCase);
        SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
        var layout=new VBoxContainer { Name="Layout" }; AddChild(layout); layout.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
        var aspect=new AspectRatioContainer
        {
            Name="Aspect",Ratio=16f/9,StretchMode=AspectRatioContainer.StretchModeEnum.Fit,
            SizeFlagsVertical=SizeFlags.ExpandFill
        };
        Video=new VideoStreamPlayer { Name="Video",Expand=true,Autoplay=false,Loop=false };
        layout.AddChild(aspect); aspect.AddChild(Video);
        var buttons=new HBoxContainer { Name="Buttons" }; layout.AddChild(buttons);
        var play=new Button { Name="PlayButton",Text=Text("Запустить","Play") };
        PauseButton=new Button { Name="PauseButton",Text=Text("Пауза","Pause") };
        var stop=new Button { Name="StopButton",Text=Text("Остановить","Stop") };
        buttons.AddChild(play); buttons.AddChild(PauseButton); buttons.AddChild(stop);
        Status=new Label { Name="Status",Text=Text("Готово к запуску","Ready") }; layout.AddChild(Status);
        play.Pressed+=() => ShowResult(LoadAndPlay());
        PauseButton.Pressed+=TogglePause;
        stop.Pressed+=StopVideo;
        Video.Finished+=() =>
        {
            FinishedCount++;
            Status.Text=Text("Воспроизведение завершено","Finished");
            PauseButton.Text=Text("Пауза","Pause");
        };
    }

    public Error LoadAndPlay()
    {
        if (!IsInsideTree()) return Error.Unconfigured;
        if (!FileAccess.FileExists(VideoPath)) return Error.FileNotFound;
        if (!VideoPath.EndsWith(".ogv",StringComparison.OrdinalIgnoreCase)) return Error.Unavailable;
        var stream=ResourceLoader.Load<VideoStream>(VideoPath);
        if (stream is null) return Error.FileUnrecognized;
        Video.Paused=false; Video.Stream=stream; Video.Play();
        PauseButton.Text=Text("Пауза","Pause");
        Status.Text=Text("Воспроизведение","Playing");
        return Error.Ok;
    }

    private void ShowResult(Error error)
    {
        if (error != Error.Ok) Status.Text=$"{Text("Ошибка","Error")}: {error}";
    }

    public void TogglePause()
    {
        if (!Video.IsPlaying()) return;
        Video.Paused=!Video.Paused;
        PauseButton.Text=Video.Paused ? Text("Продолжить","Resume") : Text("Пауза","Pause");
        Status.Text=Video.Paused ? Text("Пауза","Paused") : Text("Воспроизведение","Playing");
    }

    public void StopVideo()
    {
        Video.Stop(); Video.Paused=false;
        PauseButton.Text=Text("Пауза","Pause"); Status.Text=Text("Остановлено","Stopped");
    }

    public Error SeekTo(double seconds)
    {
        double length=Video.GetStreamLength();
        if (!double.IsFinite(seconds) || seconds<0 || length<=0 || seconds>=length) return Error.InvalidParameter;
        Video.StreamPosition=seconds;
        return Error.Ok;
    }
}

The UI chooses RU or EN when created using TranslationServer.GetLocale(). LoadAndPlay() checks the path before replacing the current stream; a missing file does not stop an already playing clip. On first use without a file, status reports the error instead of dereferencing null.

Size and aspect

Expand = true lets the container size VideoStreamPlayer instead of using the frame size as a minimum. AspectRatioContainer with Ratio = 16/9 and Fit preserves proportions. Set the corresponding Ratio for another source aspect; unused space is expected when aspects differ. Stretching the player to an arbitrary rectangle does not itself preserve aspect.

Time and completion

OperationContract
Paused = truePauses time; IsPlaying() can remain true
Paused = falseResumes from the retained position
Stop()Stops; it is not natural completion and does not emit Finished
StreamPosition = secondsThe current Theora decoder supports seeking; use a valid time inside the length
Loop = trueRepeats without emitting Finished on every loop

SeekTo() takes seconds and rejects negative values, NaN and positions at or beyond the end. Decoding may work forward from a keyframe, so seeking a long file need not be instantaneous. Check the visible frame after decoder processing; assigning time is not completion. Do not assume every VideoStream plugin has Theora's behavior.

Looping is not a guarantee of gapless sound or an invisible boundary. Keep explicit gameplay and cancellation state instead of waiting forever for a looping clip's Finished signal.

Audio and workload

Check AudioTrack, Bus, VolumeDb and the output bus. The teaching clip deliberately has a quiet audio track. Theora decodes on the CPU; measure resolution, FPS and concurrent streams on the target device. BufferingMsec does not enable hardware decoding or provide a missing codec. Assigning a network URL to VideoPath does not create HTTP streaming; this example reads local resources.

Video on a 3D surface

Obtain the live texture with GetVideoTexture() and assign it to StandardMaterial3D.AlbedoTexture. Use Unshaded if lighting should not change the video. Keep VideoStreamPlayer in the tree and playing. Do not copy each frame through GetImage() for normal display; that adds GPU readback. Use a SubViewport texture when you need an entire interface composition on the surface.

Chroma key

Save this as ChromaKey.reckfellshader, create a ShaderMaterial and assign it to the player's Material. The default key is green; threshold and positive softness control removal and edge softness. The shader retains Modulate through vertex color.

Shader
shader_type canvas_item;

uniform vec4 key_color : source_color = vec4(0.0, 1.0, 0.0, 1.0);
uniform float threshold : hint_range(0.0, 1.0) = 0.15;
uniform float softness : hint_range(0.001, 0.3) = 0.03;
varying vec4 tint;

void vertex() {
    tint = COLOR;
}

void fragment() {
    vec4 pixel = texture(TEXTURE, UV);
    float difference = distance(pixel.rgb, key_color.rgb);
    float alpha = smoothstep(threshold - softness, threshold + softness, difference);
    COLOR = pixel * tint;
    COLOR.a *= alpha;
}

This does not add an alpha channel to the Theora file; transparency is computed when displayed. Compression mixes edge colors, so a clean source background and moderate threshold matter more than trying to fix everything with one parameter.

Diagnostics

SymptomCheck
FileNotFoundName, case, Clips directory and inclusion in the export
FileUnrecognizedA real Theora codec, valid Ogg and an available decoder
Play shows nothingError output, Control size and a valid Stream
Stretched imageExpand, container Ratio and StretchMode
Time does not movePaused, speed, tree pause and frame processing
Finished never arrivesLoop or explicit stopping instead of completion
MP4 works in a browser onlyA browser decoder does not add VideoStreamPlayer support
Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.