|
204 | 204 | end for |
205 | 205 | end if |
206 | 206 |
|
207 | | - ' Remove AAC from codec list in two scenarios: |
208 | | - ' 1. ANY multichannel source (>2ch) when user has passthrough support - prevents transcoding to AAC which would downmix to stereo |
209 | | - ' 2. Unsupported AAC profiles (Main, HE-AAC) - TODO: Remove after server supports transcoding between AAC profiles |
210 | | - shouldRemoveAac = false |
| 207 | + ' Tailor the device profile's transcoding AudioCodec list to the source in two scenarios: |
| 208 | + ' 1. Multichannel source (>2ch) with passthrough support - drops stereo-output codecs so |
| 209 | + ' the server only offers surround variants (eac3/ac3/dts) in the HLS master playlist. |
| 210 | + ' Without this, Roku's HLS player has been observed to pick the stereo variant. |
| 211 | + ' 2. AAC source with unsupported profile (Main, HE-AAC) - drops AAC so transcoding |
| 212 | + ' falls through to MP3. TODO: remove once server transcodes between AAC profiles. |
| 213 | + shouldOptimize = false |
211 | 214 |
|
212 | | - ' Scenario 1: ANY multichannel source with passthrough support |
213 | | - ' Removes AAC to force server to use surround passthrough codecs (eac3, ac3, dts) instead of transcoding to stereo AAC |
214 | 215 | if channelCount > 2 and hasPassthruSupport |
215 | | - shouldRemoveAac = true |
| 216 | + shouldOptimize = true |
216 | 217 | end if |
217 | 218 |
|
218 | | - ' Scenario 2: AAC with unsupported profile |
219 | 219 | if isValid(selectedAudioStream.Codec) and LCase(selectedAudioStream.Codec) = "aac" |
220 | 220 | if isValid(selectedAudioStream.Profile) and (LCase(selectedAudioStream.Profile) = "main" or LCase(selectedAudioStream.Profile) = "he-aac") |
221 | | - shouldRemoveAac = true |
| 221 | + shouldOptimize = true |
222 | 222 | end if |
223 | 223 | end if |
224 | 224 |
|
225 | | - if shouldRemoveAac |
226 | | - removeUnsupportedAacFromProfile(postData.DeviceProfile, channelCount) |
| 225 | + if shouldOptimize |
| 226 | + optimizeAudioCodecListForSource(postData.DeviceProfile, channelCount) |
227 | 227 | end if |
228 | 228 | end if |
229 | 229 | end if |
|
516 | 516 | end if |
517 | 517 | end sub |
518 | 518 |
|
519 | | -' Removes AAC from the device profile codec list to prevent stereo downmix of multichannel audio. |
520 | | -' Also handles unsupported AAC profiles (Main, HE-AAC). |
521 | | -' For stereo sources (≤2ch), also removes surround passthrough codecs (eac3, ac3, dts) |
522 | | -' so transcoding falls through to MP3 (better compatibility + smaller files). |
523 | | -sub removeUnsupportedAacFromProfile(deviceProfile as object, channelCount as integer) |
| 519 | +' Tailors a video transcoding profile's AudioCodec list to the current source's channel count |
| 520 | +' so the server (and downstream HLS variant selection) can't fall back to a less-desirable codec. |
| 521 | +' |
| 522 | +' Multichannel source (>2ch): |
| 523 | +' Strips ALL stereo-output codecs (aac, mp3, flac, alac, pcm, lpcm, wav, vorbis). Without this, |
| 524 | +' Jellyfin's HLS muxer offers BOTH the surround codec AND a stereo fallback as separate variants |
| 525 | +' in the master playlist, and Roku's HLS player has been observed (via in-house testing on |
| 526 | +' physical hardware; no upstream tracking issue at time of writing) to pick the stereo variant — |
| 527 | +' defeating the entire point of having surround passthrough hardware. Caller must already have |
| 528 | +' verified the device has surround passthrough before invoking with channelCount > 2. |
| 529 | +' |
| 530 | +' Stereo source (≤2ch): |
| 531 | +' Strips surround passthrough codecs (eac3, ac3, dts) plus aac so transcoding falls through to |
| 532 | +' mp3. Avoids handing the server a surround target codec for content that's only 2ch anyway. |
| 533 | +sub optimizeAudioCodecListForSource(deviceProfile as object, channelCount as integer) |
524 | 534 | ' Validate inputs |
525 | 535 | if not isValid(deviceProfile) then return |
526 | 536 | if not isValid(deviceProfile.TranscodingProfiles) then return |
527 | 537 |
|
528 | | - ' Surround passthrough codecs that should be removed for stereo sources |
| 538 | + ' Codecs that, as a server transcode target, can't carry surround channels — strip these for |
| 539 | + ' multichannel sources so the server is forced to pick a surround codec from the remaining list. |
| 540 | + ' Note: this list is intentionally distinct from `stereoOutputCodecs` in deviceCapabilities.bs; |
| 541 | + ' that list represents Roku decode-path behavior (different concept), so the contents differ. |
| 542 | + stereoOnlyCodecs = ["aac", "mp3", "flac", "alac", "pcm", "lpcm", "wav", "vorbis"] |
| 543 | + ' Surround passthrough codecs — pointless for stereo sources, strip them. |
529 | 544 | surroundCodecs = ["eac3", "ac3", "dts"] |
530 | 545 |
|
531 | 546 | for each rule in deviceProfile.TranscodingProfiles |
532 | 547 | if isValid(rule.Type) and rule.Type = "Video" |
533 | 548 | if isValid(rule.AudioCodec) |
534 | | - ' Split codec list into array |
535 | 549 | codecList = rule.AudioCodec.split(",") |
536 | 550 | newCodecList = [] |
537 | 551 |
|
538 | | - ' Remove AAC always, and surround codecs for stereo sources |
539 | 552 | for each codec in codecList |
540 | 553 | skipCodec = false |
541 | 554 |
|
542 | | - ' Always skip AAC to prevent downmix |
543 | | - if codec = "aac" |
| 555 | + if channelCount > 2 and arrayHasValue(stereoOnlyCodecs, codec) |
544 | 556 | skipCodec = true |
545 | 557 | end if |
546 | 558 |
|
547 | | - ' For stereo sources (≤2ch), also skip surround codecs |
548 | | - ' This ensures transcoding goes to MP3 instead of trying AC3/EAC3 |
549 | | - if channelCount <= 2 and arrayHasValue(surroundCodecs, codec) |
| 559 | + if channelCount <= 2 and (codec = "aac" or arrayHasValue(surroundCodecs, codec)) |
550 | 560 | skipCodec = true |
551 | 561 | end if |
552 | 562 |
|
|
555 | 565 | end if |
556 | 566 | end for |
557 | 567 |
|
558 | | - ' Rebuild codec string |
| 568 | + if newCodecList.count() = 0 and codecList.count() > 0 |
| 569 | + print "[optimizeAudioCodecListForSource] WARN: AudioCodec list for container '" + rule.Container + "' became empty after optimization (source ch=" + channelCount.toStr() + ", original=" + rule.AudioCodec + "); server may reject the transcoding rule" |
| 570 | + end if |
| 571 | + |
559 | 572 | rule.AudioCodec = newCodecList.join(",") |
560 | 573 | end if |
561 | 574 | end if |
|
0 commit comments