From 63ef5bfd767e1c9ebb41c5915e853c94a9974e7a Mon Sep 17 00:00:00 2001 From: L4XB Date: Sun, 25 Oct 2026 03:31:03 +0100 Subject: [PATCH] fix: read the stream quality from HLS variants without FRAME-RATE or CODECS getStreamInfo returned a resolution only when the top variant of a master playlist had RESOLUTION, FRAME-RATE and CODECS, although playlist:format and playlist:update use only the height. RFC 8216 makes FRAME-RATE and CODECS optional, so such streams never got a quality. The parsing moves to parseStreamInfo, which tests can call without a network. --- scripts/utils.ts | 10 +++++++--- tests/utils.test.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 tests/utils.test.ts diff --git a/scripts/utils.ts b/scripts/utils.ts index 029b06bb76..27b76471af 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -99,8 +99,8 @@ export function truncate(string: string, limit: number = 100) { type StreamInfo = { resolution: { width: number; height: number } bandwidth: number - frameRate: number - codecs: string + frameRate?: number + codecs?: string } export async function getStreamInfo( @@ -162,6 +162,10 @@ export async function getStreamInfo( if (!data) return undefined + return parseStreamInfo(url, data) +} + +export function parseStreamInfo(url: string, data: string): StreamInfo | undefined { let info: StreamInfo | undefined if (url.includes('.m3u8')) { @@ -173,7 +177,7 @@ export async function getStreamInfo( if (playlist && playlist.isMasterPlaylist && playlist.variants.length) { const v = orderBy(playlist.variants, ['bandwidth'], ['desc'])[0] - if (v && v.resolution && v.frameRate && v.codecs) { + if (v && v.resolution) { info = { resolution: { width: v.resolution.width, height: v.resolution.height }, bandwidth: v.bandwidth, diff --git a/tests/utils.test.ts b/tests/utils.test.ts new file mode 100644 index 0000000000..beed94c25d --- /dev/null +++ b/tests/utils.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from 'vitest' +import { parseStreamInfo } from '../scripts/utils' + +const url = 'https://example.com/live/playlist.m3u8' + +describe('parseStreamInfo', () => { + it('reads the resolution of a master playlist without FRAME-RATE', () => { + const playlist = [ + '#EXTM3U', + '#EXT-X-STREAM-INF:BANDWIDTH=2128000,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=1280x720', + 'chunklist_720p.m3u8', + '#EXT-X-STREAM-INF:BANDWIDTH=1064000,CODECS="avc1.4d401e,mp4a.40.2",RESOLUTION=854x480', + 'chunklist_480p.m3u8' + ].join('\n') + + expect(parseStreamInfo(url, playlist)).toEqual({ + resolution: { width: 1280, height: 720 }, + bandwidth: 2128000, + frameRate: undefined, + codecs: 'avc1.4d401f,mp4a.40.2' + }) + }) + + it('reads the resolution of a master playlist without CODECS', () => { + const playlist = [ + '#EXTM3U', + '#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1024x576,FRAME-RATE=25.000', + 'tracks-v1/mono.m3u8' + ].join('\n') + + expect(parseStreamInfo(url, playlist)?.resolution).toEqual({ width: 1024, height: 576 }) + }) + + it('returns nothing for a media playlist or a master playlist without RESOLUTION', () => { + const media = ['#EXTM3U', '#EXT-X-TARGETDURATION:6', '#EXTINF:6.0,', 'segment_1.ts'].join('\n') + const audioOnly = [ + '#EXTM3U', + '#EXT-X-STREAM-INF:BANDWIDTH=128000,CODECS="mp4a.40.2"', + 'audio.m3u8' + ].join('\n') + + expect(parseStreamInfo(url, media)).toBeUndefined() + expect(parseStreamInfo(url, audioOnly)).toBeUndefined() + }) +})