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