Skip to content

Fix H.264 HW decode for AVC3 in-band streams (V4L2/m2m)#2035

Open
scottrix wants to merge 128 commits into
xbmc:Piersfrom
scottrix:fix/avc3-v4l2-hw-decode
Open

Fix H.264 HW decode for AVC3 in-band streams (V4L2/m2m)#2035
scottrix wants to merge 128 commits into
xbmc:Piersfrom
scottrix:fix/avc3-v4l2-hw-decode

Conversation

@scottrix
Copy link
Copy Markdown

Summary

On platforms using V4L2 mem2mem H.264 decoding (e.g. Raspberry Pi with OSMC's custom FFmpeg), the decoder's h264_xd_copy() function rejects avcC extradata with 0 SPS/0 PPS entries, causing a fallback to software decode. This format is used by AVC3 in-band streams (e.g. BBC iPlayer DASH) where SPS/PPS are carried in-band rather than in the codec private data.

Problem

AVC3 streams provide an avcC record like 01 64 00 1f ff e0 00 — 7 bytes with 0 SPS and 0 PPS. The V4L2 mem2mem decoder's init function (h264_xd_copy) tries to parse SPS/PPS from this record and fails with AVERROR(EINVAL), causing the decoder to fall back to software decode.

Fix

Three-part approach:

  1. Clear extradata for 0-SPS avcC — In both Session::UpdateStream() and FragmentedSampleReader::UpdateSampleDescription(), detect avcC records with 0 SPS and set extradata to null. This prevents h264_xd_copy from being called, allowing the V4L2 decoder to open successfully (it receives SPS/PPS from in-band NALUs instead).

  2. Convert avcC extradata to Annex B — For avcC records that do contain SPS, convert to Annex B format. This is already done for DRM streams (SSD_ANNEXB_REQUIRED), but non-DRM AVC1-4 streams now also get this treatment, which is required for V4L2/m2mem compatibility.

  3. Packet-level Annex B transform — Add AVCCodecHandler::Transform() override that replaces NALU length prefixes (1/2/4 byte avcC framing) with Annex B start codes (00 00 00 01). This is needed because when extradata is cleared or converted, the downstream decoder expects Annex B framed packets. The Transform() virtual already exists in CodecHandler and is called by FragmentedSampleReader::ReadSample().

Changes

  • src/Session.cpp — Restructure UpdateStream() H.264 extradata handling: DRM+SPS → Annex B conversion; avcC with 0 SPS → clear extradata; avcC with SPS → Annex B conversion; non-avcC → pass through. Added error handling for failed AvcToAnnexb() calls.
  • src/codechandler/AVCCodecHandler.h — Add Transform() override and SetAnnexBTransformNeeded()/m_needAnnexBTransform members.
  • src/codechandler/AVCCodecHandler.cpp — Implement Transform(): iterates buffer replacing NALU length prefixes with 00 00 00 01 start codes.
  • src/samplereader/FragmentedSampleReader.cpp — Add AVC1-4 handling in UpdateSampleDescription(): 0-SPS → clear extradata + enable transform; with SPS → ExtraDataToAnnexB() + enable transform; failed conversion → clear extradata + enable transform.

Testing

Tested on Raspberry Pi 4 (OSMC 2026.05-1, kernel 5.15.92, Kodi 21.3 Omega) with BBC iPlayer DASH streams:

  • V4L2 mem2mem H.264 decoder opens successfully
  • Video+audio playback works correctly
  • Stream adaptive bitrate switching works (960x540 → 1280x720)
  • No regression observed with non-AVC3 streams

CastagnaIT and others added 30 commits July 20, 2024 09:47
[backport][Bento4] Version bump (upstream sync)
[backport][DashTree] Always ensure period duration
Will be introduced on next Kodi versions
[backport][KodiProps] New drm_legacy, removed drm property
[backport] Replaced deprecated sprintf with snprintf
[backport][AdaptiveTree] IsLastSegment return false if period is the last one
[backport][Deps] Reverted recents bento4 commits
[backport] Fix wrong Dash KID data format and cleanups
src/decrypters/Helpers.h:55:13: error: 'uint8_t' was not declared in this scope
   55 | std::vector<uint8_t> ConvertKidStrToBytes(std::string_view kidStr);
[Omega/backport] Fix build error with gcc 14
CADTSSampleReader::ReadSample call ADTSReader::ReadPacket each time kodi request buffer data
but when the stream buffer reach the end the m_id3TagParser.parse cannot read data from m_stream
by returning ID3TAG::PARSE_FAIL
this was breaking the while loop and return true

since return true was causing that CADTSSampleReader::ReadSample
to return AP4_SUCCESS with bad data
where instead was needed to check for waitingForSegment and mark the m_eos as true,
this to allow stop the playback and so avoid flooding kodi demuxer of bad package data
[backport][ADTSReader] Return false when id3tagparser fails due to eos
This variable is modified within the for loop
so place it outside will cause that sideeffects
because the variable is assigned by some conditions
[Session] Restored licenseData variable on for loop
This should allow play Widevine streams by using ClearKey DRM
untested since i havent found a sample
github-actions Bot and others added 28 commits April 15, 2025 06:52
The previous code was assuming that the startnumber was the result of
(period start / duration)
now take in account that startNumber of segmenttemplate can be different
[backport][DashTree] Fix segmentNumber based on tsb
[backport][widevine] Updated CDM interface
[backport][UrlUtils] Fix Join to keep dots in a path
[backport][UrlUtils] Preserve url port number
If crypto info has been set previously, and the current package does not use it, it must be reset to prevent the decryptor from attempting to decrypt unencrypted content
[backport][FragmentedSampleReader] Reset crypto info on change
[backport][FragmentedSampleReader] Fix switching between unencrypted/crypted content
On platforms using V4L2 mem2mem H.264 decoding (e.g. Raspberry Pi with
OSMC's custom FFmpeg), the decoder's h264_xd_copy() function rejects
avcC extradata with 0 SPS/0 PPS entries, causing a fallback to software
decode. This format is used by AVC3 in-band streams (e.g. BBC iPlayer
DASH) where SPS/PPS are carried in-band rather than in the codec
private data.

Fix by:
- Detecting avcC with 0 SPS in Session::UpdateStream() and
  FragmentedSampleReader::UpdateSampleDescription(), and clearing
  extradata so the V4L2 decoder skips h264_xd_copy()
- Adding AVCCodecHandler::Transform() to convert NALU length-prefixed
  packets (avcC framing) to Annex B start-code framing at decode time
- Setting the AnnexB transform flag for all H.264 AVC1-4 formats
  (with and without SPS), ensuring consistent packet-level conversion
@kodiai
Copy link
Copy Markdown

kodiai Bot commented May 20, 2026

Decision: APPROVE

kodiai response

Decision: APPROVE
Issues: none

Evidence:

  • Review prompt covered 50 changed files.
  • Review scope note: output was scoped by prompt budget limits; Review Details include bounded counts only.
Review Details
  • Review plan: ready hash=6f3af850f967 route=standard task=review.full files=141 lines=29942(local-diff) budget=75t/1230s gates=3/3 publish=canonical-visible-surface graph=skipped candidates=preferred doctrine=disabled/0/0/0 reasons=disabled

  • Review reducer: ready input=2 kept=2 suppressed=0 rewritten=0 deprioritized=0 lowConfidence=0 auditEvents=0 severityDemoted=0 graphValidated=0 graphUncertain=0 doctrine=disabled/0/0/0 reasons=disabled

  • Review candidates: shadow recorded=2 rejected=0 errors=0 artifact=present repo=xbmc-inputstream.adaptive pr=2035 key=kodiai-review-output:v1:inst-109141824:xbmc-inputstream.adaptive:pr-2035:action- delivery=614cff70-547c-11f1-9ecc-536cab087a90

  • Review candidate publication: mode=blocked approved=2 rewritten=0 published=0 directFallback=0 reasons=none

  • Files reviewed: 141

  • Findings: 0 critical, 0 major, 0 medium, 0 minor

  • Lines changed: +23390 -6552

  • Requested profile: minimal (auto, lines changed: 29942)

  • Effective profile: minimal

  • Bounded review: covered 50/141 changed files via large-PR triage (30 full, 20 abbreviated; 91 not reviewed)

  • Timeout auto-reduction: applied

  • Contributor experience: profile-backed (using linked contributor profile guidance)

  • Shadow specialist: lane=docs-config-truth status=skipped reason=no-operator-truth-paths candidateCount=0 decisionCount=0 decisionCounts=candidate:0,duplicate:0,disagreement:0,dismissed:0,unclassifiable:0 duplicateCount=0 disagreementCount=0 dismissedCount=0 unclassifiableCount=0 truncatedCandidateCount=0 metricAvailability=token:n,cost:n,latency:n visiblePublicationDenied=true approvalPublicationDenied=true privateOnly=true shadowOnly=true redacted=raw:n,publication:n,approval:n,unsafe:0 correlationKey=a881756ad863fa32 deliveryId=614cff70-547c-11f1-9ecc-536cab0… reviewOutputKey=kodiai-review-output:v1:inst-10…

  • M072 candidate publication bridge: status=denied; bridgeVersion=candidate-publication-bridge.v1; bridgeId=candidate-publication-record:31a85b8b586ca8a4bd1e3787cd6c08db; recordKey=candidate-publication-record:31a85b8b586ca8a4bd1e3787cd6c08db; correlationKey=candidate-publication-bridge:526e5b7b796d8c0a631b21daf0d0be6b; source=review-handler-publication; candidateRef=candidate-publication-summary-f46f0c9a; verification=none; counts=candidateCount:0,evidenceCount:0,verifiedCount:0,partiallyVerifiedCount:0,unverifiedCount:0,disprovenCount:0,publicationEligibleCount:0,malformedRecordCount:0,unsafeInputFieldCount:0; reasons=no-evidence,publication-ineligible; malformed=none; presence=deliveryId:y,reviewOutputKey:y,upstreamCorrelationKey:y,policyCorrelationKey:y; handoffOwner=available; redaction=privateOnly:y,rawPayloads:n,publicationFields:n,evidencePayloads:n,githubCommentBody:n,reducerRawPayload:n,discardedRawPayload:n,discardedPublicationFields:n,discardedEvidencePayloads:n

  • Review finding lifecycle: status=normalized; counts=input:2,recorded:2,rejected:0,unsafeInputFields:0; correlation=repo:y,pull:y,reviewOutputKey:y,deliveryId:y,commit:y; statuses=detected:2,open:2,suggested:0,validated:0,revalidated:0,resolved:0,blocked:0,degraded:0; severity=critical:1,major:1,medium:0,minor:0; actionability=actionable:0,needs-human-review:2,needs-reproduction:0,blocked:0,not-actionable:0; reasons=automatic-detected,automatic-open,automatic-review; rejected=none; redaction=privateOnly:y,rawPrompts:n,rawModelOutput:n,candidateBodies:n,toolPayloads:n,secretLike:n,diffs:n,unboundedArrays:n,unsafeFields:0

  • Review validation truth: status=normalized; counts=detected:2,suggested:0,validated:0,revalidated:0,resolved:0,blocked:0,degraded:0,open:2,uncertain:0,inputFindings:2,unsafeInputFields:0; evidence=fresh:0,stale:0,missingValidation:2,missingRevalidation:2; reasons=validation-missing:2; refs=rfl-103e061ca316418b:open:validation-missing:fix:n:validation:n:revalidation:n,rfl-f38206ab63f4062e:open:validation-missing:fix:n:validation:n:revalidation:n; correlation=reviewOutputKey:y,deliveryId:y; redaction=privateOnly:y,rawPrompts:n,rawModelOutput:n,candidateBodies:n,replacementText:n,toolPayloads:n,secretLike:n,diffs:n,unboundedArrays:n,unsafeFields:0

  • Review completed: 2026-05-20T18:57:55.315Z

  • Total wall-clock: 10m 13s

  • Phase timings:

    • queue wait: 0ms
    • workspace preparation: 1.8s
    • retrieval/context assembly: 7.8s
    • executor handoff: 45s
    • remote runtime: 9m 13s
    • publication: 3.0s
  • Tokens: 186 in / 24,989 out | 0.9203

  • Review scope: Reviewed 50/141 files, prioritized by risk

  • Full review: 30 files | Abbreviated review: 20 files | Not reviewed: 91 files

Files not fully reviewed (sorted by risk score)
  • inputstream.adaptive/resources/language/resource.language.sv_se/strings.po (risk: 48)
  • inputstream.adaptive/resources/language/resource.language.zh_cn/strings.po (risk: 48)
  • src/decrypters/Helpers.h (risk: 48)
  • inputstream.adaptive/resources/language/resource.language.af_za/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.am_et/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.ar_sa/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.ast_es/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.az_az/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.be_by/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.bg_bg/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.bs_ba/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.cy_gb/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.en_au/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.en_nz/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.en_us/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.eo/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.es_ar/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.eu_es/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.fa_af/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.fa_ir/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.fo_fo/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.fr_ca/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.gl_es/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.hy_am/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.id_id/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.is_is/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.kn_in/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.lt_lt/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.ml_in/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.mn_mn/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.mt_mt/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.my_mm/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.nb_no/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.os_os/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.pt_pt/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.si_lk/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.sq_al/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.sr_rs/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.sr_rs@latin/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.szl/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.ta_in/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.tg_tj/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.th_th/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.tr_tr/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.uk_ua/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.uz_uz/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.vi_vn/strings.po (risk: 47)
  • inputstream.adaptive/resources/language/resource.language.zh_tw/strings.po (risk: 47)
  • src/common/AdaptiveStream.cpp (risk: 47)
  • src/decrypters/widevine/WVDecrypter.cpp (risk: 47)
  • src/decrypters/widevineandroid/WVDecrypter.cpp (risk: 47)
  • lib/cdm/cdm/media/cdm/cdm_adapter.h (risk: 46)
  • src/common/SegTemplate.cpp (risk: 46)
  • src/parser/SmoothTree.cpp (risk: 46)
  • src/utils/DigestMD5Utils.cpp (risk: 46)
  • lib/cdm/cdm/debug.h (risk: 45)
  • src/utils/StringUtils.h (risk: 45)
  • inputstream.adaptive/resources/language/resource.language.ro_md/strings.po (risk: 44)
  • inputstream.adaptive/resources/language/resource.language.scn/strings.po (risk: 44)
  • src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.h (risk: 44)
  • src/demuxers/ADTSReader.cpp (risk: 44)
  • src/parser/PRProtectionParser.h (risk: 44)
  • src/Session.h (risk: 43)
  • src/decrypters/widevine/WVCencSingleSampleDecrypter.h (risk: 43)
  • src/decrypters/widevineandroid/WVDecrypter.h (risk: 43)
  • src/CompKodiProps.h (risk: 42)
  • src/utils/Base64Utils.h (risk: 42)
  • src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.h (risk: 41)
  • src/utils/CurlUtils.h (risk: 41)
  • inputstream.adaptive/addon.xml.in (risk: 40)
  • src/decrypters/IDecrypter.h (risk: 40)
  • src/decrypters/clearkey/ClearKeyDecrypter.h (risk: 40)
  • src/decrypters/widevine/WVDecrypter.h (risk: 40)
  • src/utils/Utils.h (risk: 40)
  • src/common/AdaptiveDecrypter.h (risk: 39)
  • src/codechandler/AVCCodecHandler.h (risk: 38)
  • src/samplereader/FragmentedSampleReader.h (risk: 38)
  • src/test/TestUtils.cpp (risk: 37)
  • src/common/AdaptiveStream.h (risk: 36)
  • src/utils/log.h (risk: 36)
  • src/test/TestDASHTree.cpp (risk: 32)
  • depends/common/bento4/bento4.sha256 (risk: 30)
  • inputstream.adaptive/changelog.txt (risk: 24)
  • azure-pipelines.yml (risk: 23)
  • src/test/manifests/mpd/pssh_default_kid.mpd (risk: 14)
  • depends/common/bento4/bento4.txt (risk: 12)
  • depends/common/rapidjson/flags.txt (risk: 12)
  • depends/windows/dlfcn-win32/flags.txt (risk: 12)
  • depends/windowsstore/dlfcn-win32/flags.txt (risk: 12)
  • depends/common/pugixml/flags.txt (risk: 11)
  • lib/cdm/CMakeLists.txt (risk: 11)
- Keyword parsing: No keywords detected - Budget behavior: scoped (prompt-budget-limited). - Prompt budget: 6 sections, 3 trimmed, 0 bypassed, 296467 trimmed tokens. - Cache behavior: 2 observations, 1 hits, 1 misses, 0 degraded, 0 bypassed. - Continuation behavior: 0 observations, 0 compacted, 0 fallback, 0 degraded, 0 bypassed.
  • Keyword parsing: No keywords detected
  • Budget behavior: scoped (prompt-budget-limited).
  • Prompt budget: 6 sections, 3 trimmed, 0 bypassed, 296467 trimmed tokens.
  • Cache behavior: 2 observations, 1 hits, 1 misses, 0 degraded, 0 bypassed.
  • Continuation behavior: 0 observations, 0 compacted, 0 fallback, 0 degraded, 0 bypassed.

@CastagnaIT CastagnaIT added the Don't merge PR that should not be merged (yet) label May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Don't merge PR that should not be merged (yet)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants