---
name: media-streaming
description: "Self-hosted media streaming and media-content retrieval: Jellyfin on Proxmox LXC/VMs with Tailscale remote access, plus GIFs, YouTube transcripts, audio analysis, and music generation."
version: 2.0.0
author: Hermes Agent
license: MIT
platforms: [linux, macos, windows]
metadata:
  hermes:
    tags: [Jellyfin, Proxmox, Tailscale, MTU, DirectPlay, Transcoding, Streaming, Codecs, LXC, Homelab, Media-Server, GIF, YouTube, Audio, Music]
    related_skills: [media-streaming, llm-inference-and-serving]
    category: media
---

# Media Streaming

Run and troubleshoot self-hosted media servers (primarily Jellyfin) on Proxmox LXC/VMs with Tailscale-based remote access. Covers streaming performance, codec issues, client selection, hardware transcoding, and Proxmox container maintenance.

## 1. Quick Diagnostic Checklist

When a movie stalls, buffers, or loses audio over Tailscale:

1. **Check if it is just this file.** Play another large movie.
2. **Check Direct Play vs. Transcoding.** In the Jellyfin player, open the gear/info panel and look for `Direct Play` or `Transcoding`.
3. **Check the audio codec.** Jellyfin web/Mac app page → three dots → Media Info → Audio.
4. **Check MTU on the client.** macOS: `ifconfig $(route get <tailscale-ip> | awk '/interface:/ {print $2}')`.
5. **Check Tailscale path.** `tailscale status` on the server or client — look for `direct` (LAN) vs. `relay` (DERP).
6. **Check server resources.** CPU/disk I/O inside the Jellyfin container if accessible.

---

## 2. Tailscale MTU and Fragmentation

Tailscale interfaces default to **MTU 1280**. If the client OS sends larger frames, video streams can fragment, throughput collapses, and long movies stall partway through.

### Verify on the server/container

```bash
ip link show tailscale0 | grep mtu
# Expected: mtu 1280

# Large ping test with don't-fragment bit
ping -M do -s 1300 <client-tailscale-ip>
# If this fails, fragmentation is happening.
```

### Fix on macOS client

Find the Tailscale interface:

```bash
route get 100.115.66.32
# interface: utun3
```

Set MTU to 1280:

```bash
sudo ifconfig utun3 mtu 1280
```

Make it persistent with a LaunchDaemon at `/Library/LaunchDaemons/com.tailscale.mtu.plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.tailscale.mtu</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/sh</string>
        <string>-c</string>
        <string>sleep 10; /sbin/ifconfig utun3 mtu 1280</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <false/>
</dict>
</plist>
```

Load it:

```bash
sudo launchctl load /Library/LaunchDaemons/com.tailscale.mtu.plist
```

### Proxmox bridge MTU

If the container bridge MTU is 1500 while Tailscale inside is 1280, packets may fragment on the container side. Match the bridge MTU or route MTU if stalling persists. See `references/media-streaming-proxmox-network.md` for exact `pct` and `/etc/network/interfaces` commands.

---

## 3. Direct Play vs. Transcoding

**Goal:** direct-play video whenever possible. Transcoding is the most common cause of stalls and silent audio.

### How to check

In any Jellyfin client during playback:
- Click the **gear / info** icon
- Look for **Direct Play** or **Transcoding**

If it says **Transcoding**, the server CPU/disk may be the bottleneck.

### Causes of unwanted transcoding

| Cause | Fix |
|-------|-----|
| Browser playback of HEVC/H.265 | Use Jellyfin Media Player, Infuse, or VidHub |
| Forced lower bitrate in client | Set client/server max bitrate to Original / Unlimited |
| Image-based subtitles (PGS/ASS/VobSub) | Use SRT subtitles or disable subtitles |
| Audio codec client cannot decode | Transcode audio only, keep video Direct Play (Section 4) |

### Server-side bitrate settings

- Jellyfin admin → **Playback** → **Streaming**
- Set **Max streaming bitrate** to **Unlimited** or a high value (e.g. 100 Mbps).
- Per-user: allow remote media and high bitrate.

---

## 4. Silent Audio / Unsupported Codecs

If video plays but audio is silent, the audio codec is usually not decodeable by the client.

### Check Media Info

Movie page → three dots → Media Info → Audio. Common codecs:

| Codec | Typical Mac client behavior |
|-------|----------------------------|
| AAC | Works |
| AC3 / EAC3 (Dolby Digital / Dolby Digital Plus) | Often silent in browser / Jellyfin Mac app |
| TrueHD / Atmos | Usually silent |
| DTS / DTS-HD MA | Often silent |
| FLAC | Usually works |

### Force audio-only transcoding

In the Jellyfin client during playback:
- Try a different audio track.
- Disable audio passthrough / bitstreaming if present.
- Set client to allow audio transcoding.

On the server:
- Jellyfin admin → **Playback** → **Transcoding** → enable audio transcoding.
- User profile → allow audio transcoding.

This keeps the video direct and only re-encodes audio to AAC, which fixes sound with minimal CPU cost.

### Remux to AAC permanently

For a problematic file, remux the audio without re-encoding video:

```bash
ffmpeg -i "movie.mkv" -c:v copy -c:a aac -b:a 384k -map 0:v:0 -map 0:a:0 "movie-aac.mkv"
```

Use `-map 0:s:?` to keep subtitles if desired.

#### Remuxing directly on a Proxmox host

When the Jellyfin library lives in a bind mount, run `ffmpeg` on the **Proxmox host** (faster disk access than inside the LXC container) and atomically replace the file.

1. Find the host path from the container mount point:

   ```bash
   pct config <JELLYFIN_VMID> | grep mp
   # mp0: /mnt/pve/hdd-data/media,mp=/mnt/media
   # Container /mnt/media == Host /mnt/pve/hdd-data/media
   ```

2. Install `ffmpeg` on the host if missing:

   ```bash
   apt update && apt install -y ffmpeg
   ```

3. Remux to a temporary file:

   ```bash
   HOST_FILE="/mnt/pve/hdd-data/media/movies/Example.mkv"
   AAC_FILE="${HOST_FILE}.aac-remux.mkv"

   ffmpeg -y -i "$HOST_FILE" \
     -map 0:v -c:v copy \
     -map 0:a:0 -c:a aac -b:a 384k \
     -map 0:s? -c:s copy \
     -metadata title="Example" \
     "$AAC_FILE"
   ```

4. Back up the original and atomically replace:

   ```bash
   mv "$HOST_FILE" "${HOST_FILE}.original-eac3"
   mv "$AAC_FILE" "$HOST_FILE"
   ```

5. Refresh the movie's metadata in Jellyfin so the new audio codec is detected.

See `references/media-streaming-proxmox-remux.md` for a complete worked example.

---

## 5. Client Recommendations

| Client | Best for | Notes |
|--------|----------|-------|
| **Infuse** (paid) | Mac, iOS, tvOS | Excellent Direct Play, HEVC, TrueHD/DTS passthrough |
| **Jellyfin Media Player** (free) | Desktop | Better than browser, good codec support |
| **VidHub** (free) | Mac, iOS | Strong alternative to Infuse |
| Browser | Quick access only | Often forces transcoding or silent audio |

---

## 6. Hardware Transcoding

If you must transcode (e.g. remote mobile clients), use hardware acceleration.

- Jellyfin admin → **Playback** → **Transcoding**
- Select the matching accelerator:
  - Intel iGPU: **QuickSync (QSV)**
  - AMD GPU: **VAAPI** or **AMF**
  - NVIDIA: **NVENC**
- Requires GPU passthrough or access inside the LXC container.

For Proxmox LXC GPU passthrough, see `references/media-streaming-proxmox-network.md`.

---

## 7. Permanent Remote Access Setup

For reliable streaming from anywhere over Tailscale:

1. Set client Tailscale MTU to **1280**.
2. Use a native client (**Infuse** / **Jellyfin Media Player**) instead of browser.
3. Set Jellyfin server max bitrate to **Unlimited**.
4. Ensure Direct Play for video; allow audio transcoding if needed.
5. (Optional) Remux problematic files to AAC.
6. (Optional) Enable hardware transcoding for low-power clients.
7. (Optional) Match Proxmox bridge MTU to Tailscale if fragmentation persists.

---

## 8. Proxmox LXC-Specific Operations

Many self-hosted Jellyfin servers run inside Proxmox LXC containers. The container's default root disk (often 16 GiB) fills quickly with metadata, thumbnails, transcode temp files, and image caches. Jellyfin 10.11+ refuses to start if either `/var/lib/jellyfin/data` or `/var/cache/jellyfin` has less than 2 GiB free.

### Move Jellyfin data and cache to bulk storage

1. **Stop the CT**
   ```bash
   pct stop <JELLYFIN_VMID>
   ```

2. **Create destination directories on the host**
   ```bash
   mkdir -p /mnt/pve/hdd-data/jellyfin-data
   mkdir -p /mnt/pve/hdd-data/jellyfin-cache
   ```

3. **Mount the CT root disk on the host and copy existing data**

   ```bash
   losetup -fP /mnt/pve/hdd-data/images/<VMID>/vm-<VMID>-disk-0.raw
   losetup -l | grep vm-<VMID>-disk
   # Note the loop device, e.g. /dev/loop2
   ```

   Check whether the disk has partitions:
   ```bash
   fdisk -l /dev/loop2
   ```

   **If it has a Linux partition (e.g. /dev/loop2p2):**
   ```bash
   kpartx -av /dev/loop2
   mkdir -p /mnt/jellyfin-root-temp
   mount /dev/mapper/loop2p2 /mnt/jellyfin-root-temp
   ```

   **If it is a single ext4 filesystem (no partitions):**
   ```bash
   mkdir -p /mnt/jellyfin-root-temp
   mount /dev/loop2 /mnt/jellyfin-root-temp
   ```

   Copy the existing directories:
   ```bash
   cp -a /mnt/jellyfin-root-temp/var/lib/jellyfin/. /mnt/pve/hdd-data/jellyfin-data/
   cp -a /mnt/jellyfin-root-temp/var/cache/jellyfin/. /mnt/pve/hdd-data/jellyfin-cache/
   ```

   Unmount and clean up:
   ```bash
   umount /mnt/jellyfin-root-temp
   kpartx -dv /dev/loop2 2>/dev/null || true
   losetup -d /dev/loop2
   rmdir /mnt/jellyfin-root-temp
   ```

4. **Add bind mounts to the CT config**
   ```bash
   echo "mp2: /mnt/pve/hdd-data/jellyfin-data,mp=/var/lib/jellyfin" >> /etc/pve/lxc/<VMID>.conf
   echo "mp3: /mnt/pve/hdd-data/jellyfin-cache,mp=/var/cache/jellyfin" >> /etc/pve/lxc/<VMID>.conf
   ```

5. **Start the CT and verify**
   ```bash
   pct start <JELLYFIN_VMID>
   pct exec <JELLYFIN_VMID> -- systemctl status jellyfin --no-pager
   pct exec <JELLYFIN_VMID> -- df -h /var/lib/jellyfin /var/cache/jellyfin
   ```

- Proxmox access hygiene

  - Prefer one-time temporary SSH keys generated in-session; show the user the public key and let them add it to the host.
  - If permanent access is granted, ask permission before every non-Hermes action and confirm before destructive or system-level commands.
  - Remove temporary keys after the task; never store private keys in scripts or shared locations.
  - Never read or expose Jellyfin/API credentials, `authorized_keys`, or other secrets in tool output.
  - **See also `proxmox-homelab`** for general Proxmox CT/VM creation, storage/network discovery, and SSH access patterns.

### Session example

For a complete command transcript covering a HEVC/EAC3 remux, a startup crash caused by insufficient data-directory space, and the bind-mount fix, see `references/media-streaming-proxmox-fixes.md`.

---

## References

- `references/media-streaming-proxmox-remux.md` — Step-by-step Proxmox-host remux recipe for fixing silent audio while preserving HEVC HDR video.
- `references/media-streaming-proxmox-fixes.md` — Session-specific fix log: EAC3 remux, Jellyfin data/cache relocation, and mid-movie audio drop diagnosis.

## 9. Media Content Retrieval (GIFs, YouTube, Audio, Music)

When the user asks for media content (not server administration), switch to these sub-workflows:

### 9.1 GIF Search (Tenor)

```bash
curl -s "https://tenor.googleapis.com/v2/search?q=excited+dog&key=$TENOR_API_KEY&limit=5" | jq '.results[].media_formats.gif.url'
```

### 9.2 YouTube Transcripts

```python
from youtube_transcript_api import YouTubeTranscriptApi
video_id = "dQw4w9WgXcQ"
transcript = YouTubeTranscriptApi.get_transcript(video_id)
text = " ".join([s["text"] for s in transcript])
```

### 9.3 Audio Analysis

```python
import librosa
y, sr = librosa.load("audio.mp3")
mel = librosa.feature.melspectrogram(y=y, sr=sr)
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
```

### 9.4 Music Generation (HeartMuLa)

```python
from heartmula import generate
audio = generate(lyrics="...", tags=["pop", "upbeat"], duration=30)
audio.save("out.wav")
```

> **Note:** Full media-content details were once a separate skill; the practical workflows above are now maintained here. See archived `media-content` in `media/.archive/` for legacy references.

---

## Related Skills

- `productivity-toolkit` — for daily briefings, Google Workspace, Notion, etc.
- `llm-inference-and-serving` — for local model backends that might power media analysis.

