diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 769de977..49945521 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,11 @@ jobs: CI_TEST_TYPE: ${{matrix.test-type}} runs-on: ${{matrix.os}} steps: + - name: Disable apparmor because it breaks Chromium headless + run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - name: checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: read node version from .nvmrc run: echo "NVMRC=$(cat .nvmrc)" >> $GITHUB_OUTPUT @@ -47,7 +50,7 @@ jobs: run: pulseaudio -D - name: setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v5 with: node-version: '${{steps.nvm.outputs.NVMRC}}' cache: npm diff --git a/lib/mp4/probe.js b/lib/mp4/probe.js index a7045a38..a65d7d11 100644 --- a/lib/mp4/probe.js +++ b/lib/mp4/probe.js @@ -353,6 +353,50 @@ getTracks = function(init) { // and are using the default track.codec = 'mp4a.40.2'; } + } else if(/^h(vc|ev)1$/i.test(track.codec)) { + /* + Using just hvc1 or hev1 as codec string is not sufficient as canPlayType won't accept it. + It must be CODEC.PROFILE.COMPATIBILTY.LEVEL.CONSTRAINTS + See these for approach: + https://www.etsi.org/deliver/etsi_ts/103200_103299/103285/01.01.01_60/ts_103285v010101p.pdf + https://github.com/wader/fq/blob/ad0c6ecd1b79cea97bbb3048106ce309e0ec7ce0/format/mpeg/hevc_dcr.go#L24 + */ + codecConfig = codecBox.subarray(78); + codecConfigType = parseType$1(codecConfig.subarray(4, 8)); + + if (codecConfigType === 'hvcC' && codecConfig.length > 21) { + // These stored in one byte as int(2), int(1), int(5) + const profileSpaceTierIDC = codecConfig[9]; + const generalProfileSpace = ['', 'A', 'B', 'C'][profileSpaceTierIDC >> 6]; + const generalTierFlag = (profileSpaceTierIDC & 0x20) ? 'H' : 'L'; + const generalProfileIDC = profileSpaceTierIDC & 0x1f; + + const generalLevelIDC = codecConfig[20]; + + // general_profile_compatibility_flags, but in reverse bit order, in a hexadecimal representation + const profileCompatibility = parseInt(Array.from(codecConfig.subarray(10, 14)).map( + byte => byte.toString(2).padStart(8,'0') + ).join('').split('').reverse().join(''), 2).toString(16); + + // hexadecimal representation of the general_constraint_indicator_flags. + // Each byte isseparated by a '.', and trailing zero bytes may be omitted. + const contraintsBytes = Array.from(codecConfig.subarray(14, 20)).map( + byte => byte.toString(16).toUpperCase().padStart(2, '0') + ); + + // Trailing zeros removed from joined string. Leave one zero byte if there + // are multiple, as all examples are like this + track.codec = [ + track.codec, + generalProfileSpace + parseInt(generalProfileIDC, 2).toString(10), + profileCompatibility, + generalTierFlag + generalLevelIDC, + ...contraintsBytes + ].join('.').replace(/(\.00)+\.00$/, ''); + } else { + // This is a fallback. Show a warning here? + track.codec = 'hvc1.1.6.L93.B0'; + } } else { // flac, opus, etc track.codec = track.codec.toLowerCase();