Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions app/Models/MediaProbe.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,21 @@ public function playsDirectly(): ?bool
return false;
}

// 10-bit H.264 is **High 10**, which Apple's hardware decoder refuses
// outright -- the audio plays and the picture stays black.
//
// The codec name alone does not catch this: the stream is genuinely
// `h264` in a genuinely `.mp4`, so every other check passes and the
// file direct-plays into a black rectangle. The depth was already
// measured and simply never consulted.
//
// This is the same failure #305 fixed on the *transcode* side, by the
// other route: there, libx264 inherited 10-bit from the source and
// emitted High 10; here, a 10-bit file is handed over untouched.
if (($this->bit_depth ?? 8) > 8) {
return false;
}

$audio = collect($this->audio_streams ?? [])
->pluck('codec')
->filter()
Expand Down
32 changes: 32 additions & 0 deletions changelog/303-reject-ten-bit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 10-bit H.264 is not direct-playable either

#305 fixed the transcode side of the 10-bit problem: `libx264` inherited the
source's pixel format and emitted **High 10**, which Apple's hardware decoder
refuses. a5's review then noted the Midnight Gospel episodes are 10-bit HEVC
Main 10 — which prompted checking the other route.

A **10-bit H.264 `.mp4`** reaches the same failure without any transcode at
all. The stream is genuinely `h264`, in a genuinely `.mp4`, so every check
passed and the file was handed over untouched — as High 10, playing black.

The probe had recorded `bit_depth` all along. Nothing consulted it.

## Verified against a real encode

```
codec_name=h264 profile=High 10 pix_fmt=yuv420p10le bits_per_raw_sample=10
```

Before: `bit_depth=10`, `isPlayableVideo=true`. After: correctly refused, so it
routes to the transcode that now produces 8-bit High.

## Unknown depth means 8-bit

A probe that recorded no depth is treated as 8-bit, because that is
overwhelmingly what it is. Assuming otherwise would transcode every file an
older probe measured, for a case that is rare.

## Testing

3 new tests, **385 pass**. Confirmed to fail without the check, and the real
10-bit file is correctly refused.
49 changes: 48 additions & 1 deletion tests/Feature/PlaysDirectlyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ protected function setUp(): void
/**
* @param array<int, array<string, mixed>> $audio
*/
private function film(string $extension, ?string $video, array $audio = []): MediaItem
private function film(string $extension, ?string $video, array $audio = [], int $bitDepth = 8): MediaItem
{
$item = MediaItem::unresolved()->create([
'user_id' => $this->user->id,
Expand All @@ -57,6 +57,7 @@ private function film(string $extension, ?string $video, array $audio = []): Med
'probed_at' => now(),
'video_codec' => $video,
'audio_streams' => $audio,
'bit_depth' => $bitDepth,
'width' => 1920,
'height' => 1080,
]);
Expand Down Expand Up @@ -132,6 +133,52 @@ public function test_vp9_in_mp4_does_not_play_directly(): void
$this->assertFalse($film->isPlayableVideo());
}

/**
* 10-bit H.264 is **High 10**, which Apple's hardware decoder refuses.
*
* The codec name does not catch this: the stream really is `h264` in a
* real `.mp4`, so every other check passes and the file direct-plays into
* a black rectangle. Verified against a real encode — ffprobe reports
* `codec_name=h264, profile=High 10, bits_per_raw_sample=10`.
*
* This is the same failure #305 fixed on the transcode side, reached by
* the other route: there libx264 *produced* High 10 from a 10-bit source;
* here a 10-bit file is handed over untouched.
*/
public function test_ten_bit_h264_does_not_play_directly(): void
{
$film = $this->film('mp4', 'h264', [['codec' => 'aac', 'channels' => 2]], bitDepth: 10);

$this->assertFalse(
$film->isPlayableVideo(),
'10-bit H.264 is High 10 profile, which Apple hardware will not decode.',
);
}

/**
* And 8-bit is unaffected, so this does not transcode the whole library.
*/
public function test_eight_bit_h264_still_plays_directly(): void
{
$film = $this->film('mp4', 'h264', [['codec' => 'aac', 'channels' => 2]], bitDepth: 8);

$this->assertTrue($film->isPlayableVideo());
}

/**
* A probe that recorded no depth is treated as 8-bit, because that is
* overwhelmingly what it is — and assuming otherwise would transcode
* every file an older probe measured.
*/
public function test_an_unknown_bit_depth_is_treated_as_eight(): void
{
$film = $this->film('mp4', 'h264', [['codec' => 'aac', 'channels' => 2]]);

$film->probe->forceFill(['bit_depth' => null])->saveQuietly();

$this->assertTrue($film->fresh()->load('probe')->isPlayableVideo());
}

/* ---------------------------------------------------------- audio --- */

/**
Expand Down
Loading