diff --git a/app/Http/Controllers/Views/Videos/VideoController.php b/app/Http/Controllers/Views/Videos/VideoController.php index 10739ef01..6d1fb39ba 100644 --- a/app/Http/Controllers/Views/Videos/VideoController.php +++ b/app/Http/Controllers/Views/Videos/VideoController.php @@ -68,6 +68,7 @@ public function show(Request $request, $id) 'codec' => Video::ERROR_CODEC, 'malformed' => VIDEO::ERROR_MALFORMED, 'too-large' => VIDEO::ERROR_TOO_LARGE, + 'moov-atom' => VIDEO::ERROR_INVALID_MOOV_POS, ]); $fileIds = $volume->orderedFiles()->pluck('uuid', 'id'); diff --git a/app/Jobs/ProcessNewVideo.php b/app/Jobs/ProcessNewVideo.php index 41c297c6a..82486917c 100644 --- a/app/Jobs/ProcessNewVideo.php +++ b/app/Jobs/ProcessNewVideo.php @@ -132,6 +132,12 @@ public function handleFile($file, $path) return; } + if ($this->hasInvalidMoovAtomPosition($path)) { + $this->video->error = Video::ERROR_INVALID_MOOV_POS; + $this->video->save(); + return; + } + $this->video->size = File::size($path); $this->video->duration = $this->getVideoDuration($path); @@ -227,6 +233,20 @@ protected function getVideoDimensions($url) return $this->ffprobe->streams($url)->videos()->first()->getDimensions(); } + protected function hasInvalidMoovAtomPosition($sourcePath) + { + // Webm and mpeg videos don't have a moov atom + if (in_array($this->video->mimeType, ['video/mpeg', 'video/webm'])) { + return false; + } + + $process = Process::forever() + ->run("ffprobe -v trace -i '{$sourcePath}' 2>&1 | grep -o -e type:\'mdat\' -e type:\'moov\'") + ->throw(); + $output = explode("\n", $process->output()); + return !str_contains($output[0], 'moov'); + } + /** * Extract images from video. * diff --git a/app/Video.php b/app/Video.php index 54b582272..bebb988c6 100644 --- a/app/Video.php +++ b/app/Video.php @@ -68,6 +68,13 @@ class Video extends VolumeFile */ const ERROR_TOO_LARGE = 5; + /** + * Error if moov atom is not located at beginning. + * + * @var int + */ + const ERROR_INVALID_MOOV_POS = 6; + /** * The attributes that are mass assignable. * diff --git a/resources/assets/js/videos/videoContainer.vue b/resources/assets/js/videos/videoContainer.vue index 3fd523a54..fa9ac8727 100644 --- a/resources/assets/js/videos/videoContainer.vue +++ b/resources/assets/js/videos/videoContainer.vue @@ -29,6 +29,7 @@ class VideoMimeTypeError extends VideoError {} class VideoCodecError extends VideoError {} class VideoMalformedError extends VideoError {} class VideoTooLargeError extends VideoError {} +class VideoMoovAtomError extends VideoError {} // Used to round and parse the video current time from the URL, as it is stored as an int // there (without decimal dot). @@ -156,6 +157,9 @@ export default { hasTooLargeError() { return this.error instanceof VideoTooLargeError; }, + hasMoovAtomError() { + return this.error instanceof VideoMoovAtomError; + }, errorClass() { if (this.hasVideoError) { if (this.error instanceof VideoNotProcessedError) { @@ -536,6 +540,8 @@ export default { throw new VideoMalformedError(); } else if (video.error === this.errors['too-large']) { throw new VideoTooLargeError(); + } else if (video.error === this.errors['moov-atom']) { + throw new VideoMoovAtomError(); } else if (video.size === null) { throw new VideoNotProcessedError(); } diff --git a/resources/views/manual/index.blade.php b/resources/views/manual/index.blade.php index 75db8e481..8364958c6 100644 --- a/resources/views/manual/index.blade.php +++ b/resources/views/manual/index.blade.php @@ -233,6 +233,14 @@ Advanced configuration of the video annotation tool.

+

+ Fix video encoding +

+ +

+ Fix errors in video files that can cause problems in BIIGLE. +

+

Reports

Reports schema diff --git a/resources/views/manual/tutorials/videos/fix-video-encoding.blade.php b/resources/views/manual/tutorials/videos/fix-video-encoding.blade.php new file mode 100644 index 000000000..81ab4105d --- /dev/null +++ b/resources/views/manual/tutorials/videos/fix-video-encoding.blade.php @@ -0,0 +1,51 @@ +@extends('manual.base') + +@section('manual-title', 'Fix video encoding') + +@section('manual-content') +
+

+ Fix errors in video files that can cause problems in BIIGLE +

+

+ To modify the video files, download and install the tool FFmpeg. +

+

Fix MP4 moov atom position

+

+ The moov atom of an MP4 file is required by the browser to play the video correctly. If the moov atom is placed at the end of the video file, the entire file must be downloaded first before the video can be played. This can be fixed by moving the moov atom to the beginning of the file. +

+

+ To check the current position of the moov atom in an MP4 file, run the following command. +

+

+ Linux: +

+ffprobe -v trace -i input.mp4  2>&1 | grep -o -e type:\'mdat\' -e type:\'moov\'
+
+

+

+ Windows: +

+ffprobe.exe -v trace -i "input.mp4" 2>&1 | findstr "type:'mdat' type:'moov'
+
+

+

+ If type:'moov' occurs at first in the command output, the video's moov atom position is valid. Otherwise, fix the position with the command below. +

+

+ Linux: +

+ffmpeg -i input.mp4 -vcodec copy -acodec copy -movflags faststart output.mp4
+
+

+

+ Windows: +

+ffmpeg.exe -i "input.mp4" -vcodec copy -acodec copy -movflags faststart "output.mp4"
+
+

+

+ The output.mp4 file will have the moov atom at the correct position. +

+
+@endsection diff --git a/resources/views/videos/show/content.blade.php b/resources/views/videos/show/content.blade.php index faf6bf059..ae834dd7b 100644 --- a/resources/views/videos/show/content.blade.php +++ b/resources/views/videos/show/content.blade.php @@ -20,6 +20,10 @@ The video file is too large. + + The video's moov atom position is invalid.
+ See the manual for how to fix this. +
diff --git a/tests/files/test_invalid_moov_atom.mp4 b/tests/files/test_invalid_moov_atom.mp4 new file mode 100644 index 000000000..4073100f7 Binary files /dev/null and b/tests/files/test_invalid_moov_atom.mp4 differ diff --git a/tests/php/Jobs/ProcessNewVideoTest.php b/tests/php/Jobs/ProcessNewVideoTest.php index 9a26d929e..279f526eb 100644 --- a/tests/php/Jobs/ProcessNewVideoTest.php +++ b/tests/php/Jobs/ProcessNewVideoTest.php @@ -225,6 +225,32 @@ public function testHandleRemoveErrorOnSuccess() $job->handle(); $this->assertNull($video->fresh()->error); } + + public function testHasInvalidMoovAtomPosition() + { + $video = VideoTest::create(['filename' => 'test_invalid_moov_atom.mp4']); + $job = new ProcessNewVideoStub($video); + $job->passThroughMimeType = true; + $job->handle(); + $this->assertSame(Video::ERROR_INVALID_MOOV_POS, $video->fresh()->error); + } + + public function testHasInvalidMoovAtomPositionNoAtom() + { + $video = VideoTest::create(['filename' => 'test.mp4']); + $job = new ProcessNewVideoStub($video); + $video->mimeType = 'video/webm'; + $job->passThroughMimeType = true; + $job->handle(); + $this->assertEmpty($video->fresh()->error); + + $video = VideoTest::create(['filename' => 'test.mp4']); + $job = new ProcessNewVideoStub($video); + $video->mimeType = 'video/mpeg'; + $job->passThroughMimeType = true; + $job->handle(); + $this->assertEmpty($video->fresh()->error); + } } class ProcessNewVideoStub extends ProcessNewVideo @@ -234,6 +260,7 @@ class ProcessNewVideoStub extends ProcessNewVideo public $duration = 0; public $passThroughDimensions = false; public $passThroughCodec = false; + public $passThroughMimeType = false; protected function getVideoDimensions($url) { @@ -272,4 +299,12 @@ protected function generateThumbnail(string $file, int $width, int $height): Vip { return VipsImage::black(100, 100); } + + protected function hasInvalidMoovAtomPosition($source) + { + if ($this->passThroughMimeType) { + return parent::hasInvalidMoovAtomPosition($source); + } + return false; + } }