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
71 changes: 71 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Windows portable build

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
windows:
runs-on: windows-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Build self-contained Windows x64 package
shell: pwsh
run: ./src/Tools/build-portable.ps1
- uses: msys2/setup-msys2@v2
with:
install: gettext
- name: Compile translations
shell: msys2 {0}
run: |
set -euo pipefail
for po in lang/po/*.po; do
language="$(basename "$po" .po)"
destination="artifacts/mpv.net-win-x64/Locale/$language/LC_MESSAGES"
mkdir -p "$destination"
msgfmt "$po" -o "$destination/mpvnet.mo"
done
- name: Check packaged player and FFmpeg on Windows
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$package = Join-Path $PWD 'artifacts/mpv.net-win-x64'
& "$package/ffmpeg.exe" -version
if ($LASTEXITCODE) { throw 'Packaged FFmpeg failed to start.' }
$config = Join-Path $env:RUNNER_TEMP 'mpvnet-smoke-config'
New-Item $config -ItemType Directory -Force | Out-Null
$stdout = Join-Path $env:RUNNER_TEMP 'mpvnet-version.txt'
$stderr = Join-Path $env:RUNNER_TEMP 'mpvnet-errors.txt'
$process = Start-Process "$package/mpvnet.com" -ArgumentList @(
'--version', '--process-instance=multi', "--config-dir=$config"
) -WorkingDirectory $package -RedirectStandardOutput $stdout -RedirectStandardError $stderr -PassThru
if (-not $process.WaitForExit(60000)) {
$process.Kill($true)
throw 'Packaged player startup timed out.'
}
$output = Get-Content $stdout -Raw
Get-Content $stderr
Write-Output $output
if ($process.ExitCode -ne 0 -or $output -notmatch 'mpv.net v' -or $output -notmatch 'mpv v') {
throw 'Packaged player did not initialize libmpv and report its version.'
}
if (-not (Get-ChildItem "$package/Locale" -Recurse -Filter '*.mo')) {
throw 'Compiled translations are missing.'
}
- name: Upload ready-to-run portable package
uses: actions/upload-artifact@v4
with:
name: mpv.net-win-x64
path: artifacts/mpv.net-win-x64/
if-no-files-found: error
retention-days: 30
89 changes: 89 additions & 0 deletions docs/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Table of contents
* [Support](#support)
* [Config Folder](#config-folder)
* [Input and context menu](#input-and-context-menu)
* [Continuing interrupted playback](#continuing-interrupted-playback)
* [Saving a buffered video](#saving-a-buffered-video)
* [Command Line Interface](#command-line-interface)
* [Terminal](#terminal)
* [mpv.net specific commands](#mpvnet-specific-commands)
Expand Down Expand Up @@ -132,6 +134,93 @@ The most important files and folders in the config folder are:
- `script-opts` folder containing user scripts configuration files.


Continuing interrupted playback
-------------------------------

**Continue** is the first item in the right-click context menu, including legacy
custom menus. It resumes healthy paused playback without discarding its buffer.
After an interruption, or when playback is stalled, it reopens the last successfully
checkpointed file/URL at its saved fractional position. After restarting mpv.net,
click Continue to recover the last playback session. It is also available as
`script-message-to mpvnet continue` for custom key bindings.

Recovery checkpoints are written once per second while playback advances, and
immediately after seeking, pausing or changing key playback settings. They include
the original source, mpv's `watch-later-options` (independent of whether watch-later
resume is enabled), volume/mute, speed, selected tracks, subtitle/audio delays,
video adjustments, external tracks, and connection settings such as HTTP headers,
referrer and yt-dlp format. Continue starts playing even if the checkpoint was
paused. A failed connection keeps the checkpoint so Continue can be tried again
after the network returns.

The checkpoint is stored as `continue.json` in the config directory, including
source URLs and any saved HTTP headers. Writes use a flushed temporary file and
atomic replacement; `continue.json.bak` retains the previous complete checkpoint
and is used if the primary file is unreadable. Recovery does not depend on a normal
exit or shutdown handler. Delete both files to forget the saved session.

An abrupt power loss can lose progress since the last completed checkpoint
(normally up to one second, plus any storage/system delay). Exact seeking requires
a seekable source and a reachable server; deleted files, expired direct URLs,
missing external tracks, and live streams without DVR cannot be restored under
every condition. A failed or imprecise restore reports an error and preserves the
checkpoint. This restores playback state, not the previous session's temporary
downloaded cache; reconnecting may need to buffer again.


Saving a buffered video
-----------------------

Use `File > Save Buffered Video...` in the right-click context menu to save a
network video as a Matroska (`.mkv`) file with the audio track selected when the
Save dialog opens. Later track switches do not change that export. Separate audio
and video sources are combined without re-encoding; the selected audio delay is
preserved. The menu command is also available as
`script-message-to mpvnet save-buffered-video` for custom bindings and menus.

You can save before the entire movie is buffered. Complete, verified cached streams
are reused. Missing beginnings, endings, seek gaps, and audio that was not buffered
before a track switch are completed automatically in a background player. When
mpv cannot prove that the selected stream is complete, that stream's source is read
again from the beginning, rather than splicing potentially mismatched cache
fragments. This can download more than the missing intervals, especially when
audio and video share a container. Streams sharing a source are fetched together.
The foreground playback position, pause state and selected tracks remain unchanged.
The original connection headers, cookies, proxy and TLS settings are used.

The export checks cached packet continuity, reads missing sources through EOF,
and verifies that every selected stream keeps its packet count and duration after
muxing. Original source gaps and legitimately shorter audio tracks are preserved.
The resulting file is decoded before reporting success, including checking decoder
error messages even when FFmpeg exits successfully. Bad cached data is fetched
again automatically. Transient download failures
are retried up to three times. Choose a destination in the Save dialog; an existing
destination is replaced only after the complete export is verified. A canceled,
failed or truncated download leaves the destination intact. Closing mpv.net cancels
an active export. Temporary export files are removed after completion or failure.
FFmpeg is included in release packages for validation and lossless muxing. For a
source build, run `src/Tools/update-mpv.ps1` to obtain FFmpeg alongside libmpv, or
provide `ffmpeg` on PATH. No FFprobe executable is required by the application.

Network videos keep their last frame open at the end, including in playlists,
so the buffer can still be saved. Use Next to continue to the next playlist entry.
Stopping, opening another file, or exiting releases the playback buffer; it does
not survive restarting the player. An export already started retains its chosen
source and tracks independently of later foreground playback changes.

Disk caching is enabled by default and the back buffer allows 512 MiB of packet
metadata (typically sufficient for several hours). Video packets occupy disk space
in mpv's cache directory, potentially as much as the entire video, while metadata
still uses RAM. The disk cache is temporary and is removed when the video closes.
These defaults can be overridden in `mpv.conf` with `cache-on-disk` and
`demuxer-max-back-bytes`. Reduced or disabled caching makes saving read more from
the source. mpv's experimental `dump-cache` can temporarily stall playback for a
large cache export. Verification can also take time on long or high-resolution
videos, and saving requires disk space for intermediate files as well as the final
output. A source that stays unavailable must become accessible before saving can
finish successfully.


Input and context menu
----------------------

Expand Down
95 changes: 95 additions & 0 deletions src/MpvNet.Windows/GuiCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Windows;
Expand All @@ -20,6 +22,9 @@ namespace MpvNet;
public class GuiCommand
{
Dictionary<string, Action<IList<string>>>? _commands;
bool _savingBufferedVideo;
CancellationTokenSource? _saveCancellation;
Task? _saveTask;

public event Action<float>? ScaleWindow;
public event Action<string>? MoveWindow;
Expand All @@ -31,6 +36,7 @@ public class GuiCommand
public Dictionary<string, Action<IList<string>>> Commands => _commands ??= new()
{
["add-to-path"] = args => AddToPath(),
["continue"] = args => ContinuePlayback(),
["edit-conf-file"] = EditCongFile,
["load-audio"] = LoadAudio,
["load-sub"] = LoadSubtitle,
Expand All @@ -40,6 +46,7 @@ public class GuiCommand
["open-optical-media"] = Open_DVD_Or_BD_Folder,
["reg-file-assoc"] = RegisterFileAssociations,
["remove-from-path"] = args => RemoveFromPath(),
["save-buffered-video"] = args => SaveBufferedVideo(),
["scale-window"] = args => ScaleWindow?.Invoke(float.Parse(args[0], CultureInfo.InvariantCulture)),
["show-about"] = args => ShowDialog(typeof(AboutWindow)),
["show-bindings"] = args => ShowBindings(),
Expand Down Expand Up @@ -76,6 +83,94 @@ void ShowDialog(Type winType)
win?.ShowDialog();
}

async void ContinuePlayback()
{
try
{
await Player.Recovery.ContinueAsync();
}
catch (OperationCanceledException)
{
// libmpv cancels pending commands during shutdown.
}
catch (Exception ex)
{
Msg.ShowError(ex.Message);
}
}

public Task CancelVideoSaveAsync()
{
_saveCancellation?.Cancel();
return _saveTask ?? Task.CompletedTask;
}

async void SaveBufferedVideo()
{
if (_savingBufferedVideo)
{
Msg.ShowInfo(_("A buffered video is already being saved."));
return;
}

_savingBufferedVideo = true;

try
{
long generation = Player.PlaybackGeneration;

if (!Player.GetPropertyBool("demuxer-via-network"))
{
Msg.ShowInfo(_("Open a network video before saving its buffer."));
return;
}

var selection = BufferedVideo.Capture(Player, generation);

string title = Player.GetPropertyString("media-title");
title = string.Concat(title.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
title = title[..Math.Min(title.Length, 120)].TrimEnd(' ', '.');

using var dialog = new SaveFileDialog {
Title = _("Save Buffered Video"),
Filter = _("Matroska video (*.mkv)|*.mkv"),
DefaultExt = "mkv",
FileName = "video - " + title + ".mkv",
OverwritePrompt = true
};

if (dialog.ShowDialog() != DialogResult.OK)
return;

Player.CommandV("show-text", _("Saving buffered video..."), "5000");
_saveCancellation = new();
_saveTask = BufferedVideo.SaveAsync(Player, dialog.FileName, selection, _saveCancellation.Token,
new Progress<string>(message => {
if (_saveCancellation is { IsCancellationRequested: false })
Player.CommandV("show-text", message, int.MaxValue.ToString(CultureInfo.InvariantCulture));
}));
await _saveTask;
if (!_saveCancellation.IsCancellationRequested)
Msg.ShowInfo(_("Buffered video saved.") + BR2 + dialog.FileName);
}
catch (OperationCanceledException)
{
// libmpv cancels pending commands during shutdown.
}
catch (Exception ex)
{
if (_saveCancellation?.IsCancellationRequested != true) Msg.ShowError(ex.Message);
}
finally
{
if (_saveTask != null && Player.IsQuitNeeded) Player.CommandV("show-text", "");
_saveTask = null;
_saveCancellation?.Dispose();
_saveCancellation = null;
_savingBufferedVideo = false;
}
}

void LoadSubtitle(IList<string> args)
{
using var dialog = new OpenFileDialog();
Expand Down
16 changes: 16 additions & 0 deletions src/MpvNet.Windows/Resources/editor_conf.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1190,3 +1190,19 @@ file = mpv
directory = Demuxer
help = <bytesize> Controls how much the demuxer is allowed to buffer ahead.\nSuffixes such as KiB and MiB are supported.
url = https://mpv.io/manual/master/#options-demuxer-max-bytes

name = cache-on-disk
file = mpv
directory = Cache
help = Keep cached packets in a temporary disk file instead of RAM, so buffered network videos can be saved. The file is removed when playback closes.\nmpv.net default: yes
url = https://mpv.io/manual/master/#options-cache-on-disk
default = yes
option = yes
option = no

name = demuxer-max-back-bytes
file = mpv
directory = Cache
help = Maximum past data retained in the cache. With disk caching this limits packet metadata in RAM, not video bytes on disk. Raising it retains more of a watched video for saving.\nmpv.net default: 512MiB
url = https://mpv.io/manual/master/#options-demuxer-max-back-bytes
default = 512MiB
17 changes: 16 additions & 1 deletion src/MpvNet.Windows/WinForms/MainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public partial class MainForm : Form
int _taskbarButtonCreatedMessage;
int _cursorAutohide = 1000;

bool _waitingForVideoSave;
bool _contextMenuIsReady;
bool _wasMaximized;
bool _maxSizeSet;
Expand Down Expand Up @@ -69,6 +70,7 @@ public MainForm()
GuiCommand.Current.ShowMenu += GuiCommand_ShowMenu;

Player.Init(Handle, true);
Player.Recovery.Failed += message => Msg.ShowError(message);

Player.ObserveProperty("window-maximized", PropChangeWindowMaximized); // bool methods not working correctly
Player.ObserveProperty("window-minimized", PropChangeWindowMinimized); // bool methods not working correctly
Expand Down Expand Up @@ -1421,9 +1423,22 @@ protected override void OnResize(EventArgs e)
}
}

protected override void OnFormClosing(FormClosingEventArgs e)
protected override async void OnFormClosing(FormClosingEventArgs e)
{
base.OnFormClosing(e);
if (e.Cancel) return;
var save = GuiCommand.Current.CancelVideoSaveAsync();
if (!save.IsCompleted)
{
e.Cancel = true;
if (_waitingForVideoSave) return;
_waitingForVideoSave = true;
try { await save; }
catch (OperationCanceledException) { }
catch (Exception ex) { Terminal.WriteError(ex); }
Close();
return;
}

if (Player.IsQuitNeeded)
Player.CommandV("quit");
Expand Down
Loading
Loading