|
446 | 446 | ' ======================================== |
447 | 447 | ' VIDEO CODEC DETECTION (per container) |
448 | 448 | ' ======================================== |
449 | | - |
450 | | - transcodingContainers = ["ts", "mp4"] |
| 449 | + ' |
| 450 | + ' Container preference order: mp4 (HLS-fmp4) first, then ts (HLS-mpegts). This |
| 451 | + ' matches the official Jellyfin Roku app and the original jellyfin-roku fork parent. |
| 452 | + ' The order matters because Jellyfin's StreamBuilder picks FirstOrDefault when |
| 453 | + ' profile ranks tie — mp4-first unlocks the wider HLS audio target set |
| 454 | + ' (alac/flac/opus/dts/truehd in addition to aac/ac3/eac3/mp3) for surround |
| 455 | + ' transcoding scenarios where the user's hardware passthrough is detected only |
| 456 | + ' for a codec that's TS-incompatible (issue #573). |
| 457 | + transcodingContainers = ["mp4", "ts"] |
451 | 458 | containerVideoCodecs = {} |
452 | 459 |
|
453 | 460 | for each container in transcodingContainers |
|
496 | 503 | allSurroundCodecs = sixChannelPassthruCodecs |
497 | 504 | end if |
498 | 505 |
|
499 | | - ' Build optimal audio codec list for transcoding |
500 | | - ' Order: AAC (stereo), passthrough surround, multichannel decode, stereo fallbacks |
| 506 | + ' Build per-container audio codec list. The construction logic is in the pure helper |
| 507 | + ' buildVideoTranscodingAudioCodecsForContainer so it can be exhaustively unit-tested |
| 508 | + ' against a full matrix of hardware shapes (eac3+ac3 passthru, dts-only, truehd-only, |
| 509 | + ' stereo-only). The helper applies the per-container HLS-target filter that prevents |
| 510 | + ' issue #573 at the construction site. |
501 | 511 | for each container in transcodingContainers |
502 | | - audioCodecList = [] |
503 | | - |
504 | | - ' 1. AAC always first (efficient for stereo). On multichannel + passthru playback, all |
505 | | - ' stereo-output codecs (including AAC) are stripped at playback time by |
506 | | - ' optimizeAudioCodecListForSource (see items.bs). |
507 | | - audioCodecList.push("aac") |
508 | | - |
509 | | - ' 2. Add surround passthrough codecs if supported (in preference order) |
510 | | - if allSurroundCodecs.count() > 0 |
511 | | - ' Apply user's preferred codec ordering |
512 | | - if isValid(preferredCodec) and preferredCodec <> "" and preferredCodec <> "auto" |
513 | | - ' Preferred codec first (if supported) |
514 | | - if arrayHasValue(allSurroundCodecs, preferredCodec) |
515 | | - audioCodecList.push(preferredCodec) |
516 | | - end if |
517 | | - |
518 | | - ' Then other surround codecs in priority order: eac3 > ac3 > dts |
519 | | - surroundPriority = ["eac3", "ac3", "dts"] |
520 | | - for each codec in surroundPriority |
521 | | - if arrayHasValue(allSurroundCodecs, codec) and codec <> preferredCodec |
522 | | - audioCodecList.push(codec) |
523 | | - end if |
524 | | - end for |
525 | | - else |
526 | | - ' Auto mode: use default priority order (eac3 > ac3 > dts) |
527 | | - surroundPriority = ["eac3", "ac3", "dts"] |
528 | | - for each codec in surroundPriority |
529 | | - if arrayHasValue(allSurroundCodecs, codec) |
530 | | - audioCodecList.push(codec) |
531 | | - end if |
532 | | - end for |
533 | | - end if |
534 | | - end if |
535 | | - |
536 | | - ' 3. Add stereo fallback codecs if device supports them (MP3 most compatible, then lossless as fallbacks) |
537 | | - stereoFallbacks = ["mp3", "flac", "alac", "pcm"] |
538 | | - for each codec in stereoFallbacks |
539 | | - if not arrayHasValue(audioCodecList, codec) |
540 | | - ' Validate device can decode this codec at 2 channels in this container |
541 | | - if di.CanDecodeAudio({ Codec: codec, ChCnt: 2, Container: container }).Result |
542 | | - audioCodecList.push(codec) |
543 | | - end if |
| 512 | + ' Detect which stereo fallback codecs the device can decode at 2ch in this container. |
| 513 | + ' Pre-computing this lets the construction helper stay pure. |
| 514 | + decodableStereoFallbacks = [] |
| 515 | + for each codec in ["mp3", "flac", "alac", "opus"] |
| 516 | + if di.CanDecodeAudio({ Codec: codec, ChCnt: 2, Container: container }).Result |
| 517 | + decodableStereoFallbacks.push(codec) |
544 | 518 | end if |
545 | 519 | end for |
546 | 520 |
|
| 521 | + audioCodecList = buildVideoTranscodingAudioCodecsForContainer(container, allSurroundCodecs, preferredCodec, decodableStereoFallbacks) |
| 522 | + |
547 | 523 | ' Create single profile per container |
548 | 524 | profile = { |
549 | 525 | "Container": container, |
|
570 | 546 | return transcodingProfiles |
571 | 547 | end function |
572 | 548 |
|
| 549 | +' Mirror of Jellyfin server's _supportedHlsAudioCodecs* from |
| 550 | +' MediaBrowser.Model/Dlna/StreamBuilder.cs. Codecs outside these per-container sets |
| 551 | +' get silently stripped by the server's HLS-codec filter when building the |
| 552 | +' TranscodingUrl, leaving an empty AudioCodec field that triggers the server's |
| 553 | +' InferAudioCodec("m3u8") fallback chain (issue #573). Listing only codecs the |
| 554 | +' server will actually use as HLS transcode targets keeps the profile honest and |
| 555 | +' prevents the bug class at the construction site. |
| 556 | +function getHlsTranscodeTargetsForContainer(container as string) as object |
| 557 | + if container = "ts" |
| 558 | + return ["aac", "ac3", "eac3", "mp3"] |
| 559 | + end if |
| 560 | + if container = "mp4" |
| 561 | + return ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dts", "truehd"] |
| 562 | + end if |
| 563 | + return [] |
| 564 | +end function |
| 565 | + |
| 566 | +' Build the AudioCodec list for a single HLS video transcoding profile container. |
| 567 | +' Pure function — all inputs explicit so the full hardware matrix can be unit-tested |
| 568 | +' without mocking roDeviceInfo or m.global. |
| 569 | +' |
| 570 | +' Ordering rules (leftmost is the server's first-choice transcode target): |
| 571 | +' 1. AAC (universal Roku decode, in both ts and mp4 server target sets) |
| 572 | +' 2. User's preferredCodec if set, device-passthru-supported, and server-target |
| 573 | +' 3. Other surround codecs in priority order (eac3 > ac3 > dts > truehd), each |
| 574 | +' gated by device passthru AND server-target for this container |
| 575 | +' 4. Stereo fallback codecs the device decodes (mp3 > flac > alac > opus), each |
| 576 | +' gated by server-target for this container |
| 577 | +' |
| 578 | +' @param container - "ts" or "mp4" |
| 579 | +' @param allSurroundCodecs - array of surround codec names with device passthrough |
| 580 | +' @param preferredCodec - user's playbackPreferredMultichannelCodec value, or "auto" / "" |
| 581 | +' @param decodableStereoFallbacks - stereo fallback codec names the device can decode |
| 582 | +' at 2ch in this container (subset of mp3/flac/alac/opus) |
| 583 | +' @returns array of codec name strings in preference order |
| 584 | +function buildVideoTranscodingAudioCodecsForContainer(container as string, allSurroundCodecs as object, preferredCodec as string, decodableStereoFallbacks as object) as object |
| 585 | + serverTargets = getHlsTranscodeTargetsForContainer(container) |
| 586 | + if serverTargets.count() = 0 then return [] |
| 587 | + |
| 588 | + surroundPriority = ["eac3", "ac3", "dts", "truehd"] |
| 589 | + audioCodecList = [] |
| 590 | + |
| 591 | + ' 1. AAC always first |
| 592 | + audioCodecList.push("aac") |
| 593 | + |
| 594 | + ' 2. User's preferred surround codec, if both device-supported and server-supported |
| 595 | + if isValid(preferredCodec) and preferredCodec <> "" and preferredCodec <> "auto" |
| 596 | + if arrayHasValue(allSurroundCodecs, preferredCodec) and arrayHasValue(serverTargets, preferredCodec) |
| 597 | + audioCodecList.push(preferredCodec) |
| 598 | + end if |
| 599 | + end if |
| 600 | + |
| 601 | + ' 3. Other surround codecs in priority order |
| 602 | + for each codec in surroundPriority |
| 603 | + if not arrayHasValue(audioCodecList, codec) and arrayHasValue(allSurroundCodecs, codec) and arrayHasValue(serverTargets, codec) |
| 604 | + audioCodecList.push(codec) |
| 605 | + end if |
| 606 | + end for |
| 607 | + |
| 608 | + ' 4. Stereo fallbacks — caller already verified device decode capability |
| 609 | + for each codec in decodableStereoFallbacks |
| 610 | + if not arrayHasValue(audioCodecList, codec) and arrayHasValue(serverTargets, codec) |
| 611 | + audioCodecList.push(codec) |
| 612 | + end if |
| 613 | + end for |
| 614 | + |
| 615 | + return audioCodecList |
| 616 | +end function |
| 617 | + |
573 | 618 | function getContainerProfiles() as object |
574 | 619 | containerProfiles = [] |
575 | 620 |
|
|
0 commit comments