Skip to content

Player Quality & Audio ​

Phase: P3B-S8

Phlix supports flexible quality selection, multi-audio tracks, subtitle selection, and Picture-in-Picture (PiP) playback.

Quality Selector ​

Quality Levels ​

LabelResolutionBitrate (SD)Bitrate (HD)
AutoDevice-dependentVariableVariable
480p854×4801.5 Mbps—
720p1280×7203 Mbps4 Mbps
1080p1920×10805 Mbps8 Mbps
4K3840×216015 Mbps20 Mbps
OriginalSource resolutionSourceSource

Quality API ​

http
GET /api/v1/media/{id}/playback?quality=720p

Per-Track Quality Override ​

Individual tracks can have different quality settings:

json
{
  "tracks": [
    {
      "id": "video-720p",
      "type": "video",
      "quality": "720p",
      "codec": "h264",
      "bitrate": 3000000
    },
    {
      "id": "video-1080p",
      "type": "video",
      "quality": "1080p",
      "codec": "h264",
      "bitrate": 8000000
    }
  ]
}

ABR (Adaptive Bitrate) ​

Auto quality uses adaptive bitrate streaming:

  1. Client measures available bandwidth
  2. Requests appropriate quality segment
  3. Switches quality without interruption
  4. Buffer target: 30 seconds

Audio Track Selection ​

Track Properties ​

PropertyDescriptionExample
languageISO 639-1 codeen, es, fr
codecAudio codecaac, mp3, opus
channelsChannel layout2.0, 5.1, 7.1
bitrateAudio bitrate128, 256, 320 kbps

Channel Layouts ​

LayoutSpeakersUse Case
2.0StereoMost content
5.1Front L/R, Center, Surround L/R, SubMovies
7.1+ Rear L/RPremium content

Audio Selection UI ​

┌─────────────────────────────┐
│ Audio Track            [×] │
├─────────────────────────────┤
│ ○ English (AAC 5.1)    [★] │
│ ○ Spanish (AAC 2.0)        │
│ ○ French (AAC 2.0)         │
│ ○ Commentary (AAC 2.0)     │
├─────────────────────────────┤
│ Current: English (AAC 5.1) │
└─────────────────────────────┘

API Usage ​

http
GET /api/v1/media/{id}/playback?audio=es
POST /api/v1/media/{id}/playback/audio
{ "trackId": "audio-es-001" }

Multi-Audio HLS ​

HLS Master Playlist ​

m3u8
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1280x720
720p.m3u8

# Audio groups
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="English",LANGUAGE="en",URI="audio-en.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="Spanish",LANGUAGE="es",URI="audio-es.m3u8"

Audio-Only HLS Playlists ​

For audio-only streams (music, podcasts):

m3u8
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-TYPE:AUDIO
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10.0
segment0.aac
#EXTINF:10.0
segment1.aac
#EXT-X-ENDLIST

Subtitle Track Selection ​

Subtitle Types ​

TypeSourceFormat
EmbeddedVideo containerSRT, ASS, SSA in container
SideloadedExternal fileSRT, VTT, ASS
Closed (CC)BroadcastCEA-608/708
HEVC SEIVideo streamBurned-in

Subtitle Selection UI ​

┌─────────────────────────────┐
│ Subtitles / CC         [×] │
├─────────────────────────────┤
│ [✓] Off                     │
│ [ ] English (SRT)           │
│ [ ] Spanish (SRT)          │
│ [ ] French (VTT)           │
│ [ ] English CC (CEA-608)    │
│ [ ] English (Burned-in)     │
├─────────────────────────────┤
│ Text Size: [●●●○○] Medium   │
│ Text Color: [White]         │
│ Background: [Semi]          │
└─────────────────────────────┘

API Usage ​

http
GET /api/v1/media/{id}/playback?subtitles=en
POST /api/v1/media/{id}/playback/subtitles
{ "trackId": "sub-en-001", "mode": "external" }

Subtitle Modes ​

ModeBehavior
offNo subtitles
externalDisplay from sideloaded file
burnedUse baked-in subtitles from video
ccUse closed captions from stream

Picture-in-Picture (PiP) ​

Browser Support ​

BrowserVersionNotes
Chrome71+Full support
Firefox79+Full support
Safari13+macOS Safari only
Edge79+Chromium-based

PiP API ​

typescript
// Enter PiP
const video = document.querySelector('video');
try {
  await video.requestPictureInPicture();
} catch (err) {
  console.error('PiP failed:', err);
}

// Exit PiP
document.exitPictureInPicture();

// Listen for events
video.addEventListener('enterpictureinpicture', (e) => {
  console.log('Entered PiP:', e.pictureInPictureWindow);
});

video.addEventListener('leavepictureinpicture', () => {
  console.log('Left PiP');
});

PiP Controls ​

When in PiP mode:

  • Play/Pause — Toggle playback
  • Skip — Seek forward/back 10 seconds
  • Next/Previous — Skip to next/previous track (in playlists)
  • Close — Exit PiP and return to main player

Limitations ​

  1. One PiP window per browser context
  2. DRM content may not support PiP
  3. Audio continues when PiP is minimized
  4. Touch devices may have limited PiP support

Mobile Considerations ​

  • iOS: PiP requires PictureInPicture capability in entitlements
  • Android: PiP works in Chrome for Android with proper manifest

Complete Playback Session Example ​

http
GET /api/v1/media/{id}/playback
json
{
  "sessionId": "sess_abc123",
  "manifest": "/stream/{id}/master.m3u8",
  "quality": "auto",
  "audio": {
    "tracks": [
      { "id": "en-aac-51", "language": "en", "codec": "aac", "channels": "5.1", "default": true },
      { "id": "es-aac-20", "language": "es", "codec": "aac", "channels": "2.0", "default": false }
    ],
    "selected": "en-aac-51"
  },
  "subtitles": {
    "tracks": [
      { "id": "off", "label": "Off", "type": "none" },
      { "id": "en-srt", "language": "en", "codec": "srt", "type": "external" }
    ],
    "selected": "off"
  },
  "trickplay": {
    "enabled": true,
    "spriteUrl": "/stream/{id}/trickplay.jpg",
    "thumbWidth": 360,
    "thumbHeight": 90
  }
}

Client Capability Negotiation ​

A client can tell the server which codecs it is able to decode by sending an X-Phlix-Client-Capabilities request header on playback-info requests. The value is a JSON codec-support map, for example:

http
X-Phlix-Client-Capabilities: {"eac3":false,"aac":true}

When the header is present, the server's direct_play verdict reflects whether the client can decode the item's (first/default) audio codec — a client that declares it cannot decode e.g. E-AC-3 is steered to transcode instead of direct play, avoiding a "video plays but audio is silent" result. When the header is absent, empty, or malformed, direct_play keeps its previous always-true behavior, so existing clients are unaffected.

Loudness Normalization ​

The server can apply EBU R128 loudness normalization (loudnorm) to transcoded audio so volume is consistent across titles. It is disabled by default and enabled by an operator in config/ffmpeg.php (loudness.enabled = true, with I/LRA/TP targets). See Config files → Loudness normalization.

Because normalization is an audio filter, it applies only to re-encoded audio. Rungs that copy the source audio stream (the original variant) and direct-play sessions are not normalized — a copied stream is never decoded, so no filter can be applied to it.

Browser HLS Compatibility ​

Web playback uses hls.js with Media Source Extensions (MSE), which is stricter than a native player. For a transcoded stream to play in a normal browser, three things must line up:

  • 8-bit H.264 video. 10-bit ("High 10") output cannot be decoded by any browser; Phlix forces 8-bit (yuv420p) on all software transcode paths.
  • Stereo AAC audio. Re-encoded HLS audio is downmixed to stereo (-ac 2). Surround layouts — in particular AC-3 5.1(side) sources — otherwise encode to AAC with a PCE and channel_configuration=0 in the ADTS header, which hls.js cannot parse: it fails to build the audio SourceBuffer and the entire player load errors even when the video is valid. Direct-play / stream-copy (original) rungs keep the source layout untouched.
  • CSP blob: allowances. The server's Content-Security-Policy allows media-src 'self' blob: and worker-src 'self' blob: so hls.js can attach its MSE blob: object URL and spawn its transmux Web Worker. If these are stripped (e.g. by a reverse proxy rewriting the CSP), the browser rejects playback with MEDIA_ELEMENT_ERROR: Media load rejected by URL safety check.

See Also ​

BSD-3-Clause