Skip to content

Commit 228211f

Browse files
committed
feat(player): enhance Windows support with improved socket handling and video output configuration
1 parent da02d3a commit 228211f

4 files changed

Lines changed: 84 additions & 16 deletions

File tree

internal/player/player.go

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,13 @@ func StartVideo(link string, args []string) (string, error) {
424424
var socketPath string
425425

426426
if runtime.GOOS == "windows" {
427-
socketPath = fmt.Sprintf(`\\.\pipe\goanime_mpvsocket_%s`, randomNumber)
427+
// Keep pipe name short — Windows named-pipe path limit is generous, but
428+
// shorter names avoid rare third-party filter issues on minimal VMs.
429+
pipeID := randomNumber
430+
if len(pipeID) > 16 {
431+
pipeID = pipeID[:16]
432+
}
433+
socketPath = fmt.Sprintf(`\\.\pipe\goanime_mpv_%s`, pipeID)
428434
} else {
429435
// Use os.TempDir() for cross-platform compatibility
430436
// macOS uses /var/folders/... accessed via $TMPDIR
@@ -434,7 +440,6 @@ func StartVideo(link string, args []string) (string, error) {
434440

435441
mpvArgs := []string{
436442
"--no-terminal",
437-
"--quiet",
438443
"--force-window=yes",
439444
fmt.Sprintf("--input-ipc-server=%s", socketPath),
440445
}
@@ -454,7 +459,8 @@ func StartVideo(link string, args []string) (string, error) {
454459
cmd := exec.Command(mpvPath, mpvArgs...)
455460
setProcessGroup(cmd) // Handle OS-specific process groups
456461

457-
// Capture stderr for better error reporting
462+
// Capture stderr so crash/GPU errors are visible when IPC never appears.
463+
// Intentionally not using --quiet: silent exits on VMs hide the real cause.
458464
var stderr bytes.Buffer
459465
cmd.Stderr = &stderr
460466

@@ -463,17 +469,32 @@ func StartVideo(link string, args []string) (string, error) {
463469
return "", fmt.Errorf("failed to start mpv: %w (stderr: %s)", err, stderr.String())
464470
}
465471

466-
util.Debugf("mpv started, waiting for socket creation: %s", socketPath)
472+
// Reap the process in the background so we can detect early exit (common on
473+
// Windows VMs when --vo=gpu fails / bundled mpv is missing DLLs). Without
474+
// this, StartVideo waits the full timeout then Process.Kill returns
475+
// "Access is denied" because the process is already dead.
476+
waitErrCh := make(chan error, 1)
477+
go func() {
478+
waitErrCh <- cmd.Wait()
479+
}()
480+
481+
util.Debugf("mpv started (pid probe), waiting for socket creation: %s", socketPath)
467482

468-
// Wait for socket creation with adaptive timeout and exponential backoff
469-
// Total max wait time: ~8 seconds (accommodates slow network streams)
470-
// Initial intervals are very short for fast local files, then back off for streams
471-
maxWaitTime := 8 * time.Second
483+
// Wait for socket creation with adaptive timeout and exponential backoff.
484+
// VMs / cold media opens can take longer than a local GPU host.
485+
maxWaitTime := 12 * time.Second
472486
initialInterval := 5 * time.Millisecond
473487
maxInterval := 100 * time.Millisecond
474488
currentInterval := initialInterval
475489

476490
for time.Since(startTime) < maxWaitTime {
491+
// Detect early mpv death before wasting the full timeout.
492+
select {
493+
case waitErr := <-waitErrCh:
494+
return "", formatMPVEarlyExitError(waitErr, stderr.String(), mpvPath)
495+
default:
496+
}
497+
477498
// Try to connect to the socket instead of checking file existence
478499
// This works for both Unix sockets and Windows named pipes
479500
conn, err := dialMPVSocket(socketPath)
@@ -483,7 +504,6 @@ func StartVideo(link string, args []string) (string, error) {
483504
return socketPath, nil
484505
}
485506

486-
// Check if MPV process is still running
487507
if cmd.Process == nil {
488508
return "", fmt.Errorf("mpv process not started properly: %s", stderr.String())
489509
}
@@ -495,13 +515,42 @@ func StartVideo(link string, args []string) (string, error) {
495515

496516
elapsed := time.Since(startTime)
497517
util.Debugf("Timeout after %.2fs waiting for mpv socket", elapsed.Seconds())
518+
if stderr.Len() > 0 {
519+
util.Debugf("mpv stderr during timeout: %s", stderr.String())
520+
}
498521

499-
// Cleanup if timeout occurs
500-
if killErr := cmd.Process.Kill(); killErr != nil {
501-
util.Debugf("Failed to kill mpv process: %v", killErr)
522+
// Best-effort cleanup. On Windows Kill returns "Access is denied" if the
523+
// process already exited between the last probe and here — ignore that.
524+
if cmd.Process != nil {
525+
if killErr := cmd.Process.Kill(); killErr != nil {
526+
util.Debugf("Failed to kill mpv process (often already exited): %v", killErr)
527+
}
502528
}
529+
// Drain Wait so we don't leak the reaper goroutine's result unnoticed.
530+
select {
531+
case waitErr := <-waitErrCh:
532+
if waitErr != nil {
533+
util.Debugf("mpv wait after timeout: %v", waitErr)
534+
}
535+
case <-time.After(500 * time.Millisecond):
536+
}
537+
538+
stderrHint := strings.TrimSpace(stderr.String())
539+
if stderrHint != "" {
540+
return "", fmt.Errorf("timeout waiting for mpv socket after %.1fs.\nmpv stderr: %s\nmpv path: %s\nPossible issues:\n1. MPV crashed or GPU/video output failed (common on VMs — try updating GPU drivers or reinstall mpv)\n2. MPV installation corrupted / missing DLLs\n3. Invalid video URL\nCheck debug logs with -debug flag", elapsed.Seconds(), stderrHint, mpvPath)
541+
}
542+
return "", fmt.Errorf("timeout waiting for mpv socket after %.1fs.\nmpv path: %s\nPossible issues:\n1. MPV hung before creating IPC (GPU/driver issue on VMs)\n2. MPV installation corrupted / missing DLLs\n3. Invalid video URL\nRun: \"%s\" --version\nCheck debug logs with -debug flag", elapsed.Seconds(), mpvPath, mpvPath)
543+
}
503544

504-
return "", fmt.Errorf("timeout waiting for mpv socket after %.1fs. Possible issues:\n1. Slow network connection - video source may be unresponsive\n2. MPV installation corrupted\n3. Firewall blocking IPC\n4. Invalid video URL\nCheck debug logs with -debug flag", elapsed.Seconds())
545+
// formatMPVEarlyExitError builds a clear error when mpv dies before the IPC
546+
// socket appears. This is the common failure mode on minimal Windows VMs.
547+
func formatMPVEarlyExitError(waitErr error, stderrOut, mpvPath string) error {
548+
stderrOut = strings.TrimSpace(stderrOut)
549+
util.Debugf("mpv exited before IPC socket was ready: %v stderr=%q path=%s", waitErr, stderrOut, mpvPath)
550+
if stderrOut != "" {
551+
return fmt.Errorf("mpv exited before IPC socket was ready: %v\nmpv stderr: %s\nmpv path: %s\nHint: on Windows VMs OpenGL often fails — GoAnime uses a VO fallback chain; if this persists, reinstall mpv or run mpv manually", waitErr, stderrOut, mpvPath)
552+
}
553+
return fmt.Errorf("mpv exited before IPC socket was ready: %v\nmpv path: %s (no stderr captured)\nHint: bundled mpv may be missing DLLs, or video output failed. Run: \"%s\" --version", waitErr, mpvPath, mpvPath)
505554
}
506555

507556
// MpvSendCommand is a wrapper function to expose mpvSendCommand to other packages

internal/player/playvideo.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ type playbackArgsInput struct {
105105
ResumeTime int
106106
}
107107

108+
// defaultVideoOutputArg picks an mpv --vo chain that survives minimal Windows
109+
// VMs (no OpenGL / only Basic Display Adapter). Non-Windows keeps plain gpu.
110+
func defaultVideoOutputArg() string {
111+
if runtime.GOOS == "windows" {
112+
return "--vo=gpu,direct3d,sdl"
113+
}
114+
return "--vo=gpu"
115+
}
116+
108117
// buildPlaybackArgs assembles the full mpv argument list from resolved inputs.
109118
// It is pure (no I/O, no globals, no prompts) so the exact argument set — and in
110119
// particular that HLS streams carry BOTH the Referer header and the
@@ -127,7 +136,9 @@ func buildPlaybackArgs(in playbackArgsInput) []string {
127136
mpvArgs = append(mpvArgs,
128137
"--no-config",
129138
"--hwdec=auto-safe",
130-
"--vo=gpu",
139+
// Windows VMs often lack working OpenGL; fall through to Direct3D/SDL
140+
// so playback still starts instead of mpv exiting before IPC.
141+
defaultVideoOutputArg(),
131142
"--profile=fast",
132143
"--video-latency-hacks=yes",
133144
)

internal/player/playvideo_args_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ func TestBuildPlaybackArgs(t *testing.T) {
183183
assert.NotContains(t, args, "--no-config", "upscaling must not use the standard profile")
184184
})
185185

186+
t.Run("default VO uses Windows fallback chain", func(t *testing.T) {
187+
args := buildPlaybackArgs(playbackArgsInput{VideoURL: "/tmp/x.mp4"})
188+
assert.Contains(t, args, defaultVideoOutputArg())
189+
assert.Contains(t, args, "--hwdec=auto-safe")
190+
})
191+
186192
t.Run("resume adds --start for non-HLS but not for HLS", func(t *testing.T) {
187193
nonHLS := buildPlaybackArgs(playbackArgsInput{VideoURL: "/tmp/x.mp4", ResumeTime: 30})
188194
assert.Contains(t, nonHLS, "--start=+30")

internal/player/socket_windows.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ func dialMPVSocket(socketPath string) (net.Conn, error) {
2424
socketPath = `\\.\pipe\` + filepath.Base(socketPath)
2525
}
2626

27-
// Use winio to connect to Windows named pipe
28-
timeout := 5 * time.Second
27+
// Short probe timeout: ERROR_FILE_NOT_FOUND returns immediately; the
28+
// timeout only matters for ERROR_PIPE_BUSY. Keep it low so StartVideo can
29+
// poll process-exit between dial attempts instead of blocking 5s.
30+
timeout := 200 * time.Millisecond
2931
return winio.DialPipe(socketPath, &timeout)
3032
}

0 commit comments

Comments
 (0)