Skip to content

Commit

Permalink
Add model fields for video width and height
Browse files Browse the repository at this point in the history
  • Loading branch information
crgwbr committed May 24, 2024
1 parent 719b64b commit 6e8e55e
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.13 on 2024-05-17 17:02

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("app", "0003_alter_customvideotrack_file_and_more"),
]

operations = [
migrations.AddField(
model_name="customvideomodel",
name="height",
field=models.IntegerField(editable=False, null=True, verbose_name="height"),
),
migrations.AddField(
model_name="customvideomodel",
name="width",
field=models.IntegerField(editable=False, null=True, verbose_name="width"),
),
]
4 changes: 4 additions & 0 deletions tests/test_admin_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ def test_add(self):
self.assertTrue(video.thumbnail)
self.assertTrue(video.duration)
self.assertTrue(video.file_size)
self.assertTrue(video.width)
self.assertTrue(video.height)

# Test that it was placed in the root collection
root_collection = Collection.get_first_root_node()
Expand Down Expand Up @@ -136,6 +138,8 @@ def test_add_no_ffmpeg(self, ffmpeg_installed):

self.assertFalse(video.thumbnail)
self.assertFalse(video.duration)
self.assertFalse(video.width)
self.assertFalse(video.height)

def test_add_no_file_selected(self):
response = self.post(
Expand Down
23 changes: 23 additions & 0 deletions wagtailvideos/migrations/0015_video_height_video_width.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.13 on 2024-05-17 17:02

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("wagtailvideos", "0014_alter_videotrack_file_alter_videotrack_kind_and_more"),
]

operations = [
migrations.AddField(
model_name="video",
name="height",
field=models.IntegerField(editable=False, null=True, verbose_name="height"),
),
migrations.AddField(
model_name="video",
name="width",
field=models.IntegerField(editable=False, null=True, verbose_name="width"),
),
]
2 changes: 2 additions & 0 deletions wagtailvideos/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class AbstractVideo(CollectionMember, index.Indexed, models.Model):
tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags'))

file_size = models.PositiveIntegerField(null=True, editable=False)
width = models.IntegerField(verbose_name=_("width"), editable=False, null=True)
height = models.IntegerField(verbose_name=_("height"), editable=False, null=True)

objects = VideoQuerySet.as_manager()

Expand Down
18 changes: 13 additions & 5 deletions wagtailvideos/transcoders/ffmpeg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,22 @@ def get_system_checks(self):

def update_video_metadata(self, video) -> None:
has_changed = video._initial_file is not video.file
filled_out = video.thumbnail is not None and video.duration is not None
meta_fields = ["thumbnail", "duration", "width", "height"]
filled_out = all(
(getattr(video, field) is not None)
for field in meta_fields
)
if not has_changed and filled_out:
return
with self._get_local_file(video.file) as file_path:
if has_changed or not video.thumbnail:
video.thumbnail = ffmpeg.get_thumbnail(file_path)
if has_changed or video.duration is None:
video.duration = ffmpeg.get_duration(file_path)
stats = ffmpeg.get_stats(file_path)
for field in meta_fields:
if has_changed or not getattr(video, field):
if field == "thumbnail":
video.thumbnail = ffmpeg.get_thumbnail(file_path)
elif stats:
value = stats.get(field)
setattr(video, field, value)

def do_transcode(self, transcode):
if self.blocking:
Expand Down
112 changes: 75 additions & 37 deletions wagtailvideos/transcoders/ffmpeg/ffmpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import logging
import os
import os.path
import re
import shutil
import subprocess
import tempfile
from typing import List, Optional
import json
from typing import List, Optional, TypedDict

from django.conf import settings
from django.core.files.base import ContentFile
Expand All @@ -16,51 +16,85 @@
logger = logging.getLogger(__name__)


class VideoStats(TypedDict):
width: int
height: int
duration: datetime.timedelta


def installed(path: Optional[str] = None) -> bool:
return shutil.which("ffmpeg", path=path) is not None


def get_duration(file_path: str) -> Optional[datetime.timedelta]:
def get_stats(file_path: str) -> Optional[VideoStats]:
if not installed():
raise RuntimeError('ffmpeg is not installed')

cmd = ["ffprobe", file_path, '-show_format', '-v', 'quiet']
raise RuntimeError("ffmpeg is not installed")
cmd = [
"ffprobe",
file_path,
# Return output in JSON
"-of",
"json",
# Quiet mode
"-v",
"quiet",
# Ignore audio tracks
"-select_streams",
"v",
# Return video format info
"-show_format",
# Return video dimensions
"-show_entries",
"stream=width,height",
]
try:
with open(os.devnull, "r+b") as fnull:
show_format_bytes = subprocess.check_output(
cmd,
stdin=fnull,
stderr=fnull,
resp = json.loads(
subprocess.check_output(
cmd,
stdin=fnull,
stderr=fnull,
)
)
except subprocess.CalledProcessError:
logger.exception("Getting video duration failed")
return None
show_format = show_format_bytes.decode("utf-8")
# show_format comes out in key=value pairs seperated by newlines
duration = re.findall(r'([duration^=]+)=([^=]+)(?:\n|$)', show_format)[0][1]
return datetime.timedelta(seconds=float(duration))
stream = resp["streams"][0]
return VideoStats(
width=int(stream["width"]),
height=int(stream["height"]),
duration=datetime.timedelta(seconds=float(resp["format"]["duration"])),
)


def get_thumbnail(file_path: str) -> Optional[ContentFile]:
if not installed():
raise RuntimeError('ffmpeg is not installed')
raise RuntimeError("ffmpeg is not installed")

file_name = os.path.basename(file_path)
thumb_extension = getattr(settings, 'WAGTAIL_VIDEOS_THUMBNAIL_EXTENSION', 'jpg').lower()
thumb_name = '{}_thumb.{}'.format(os.path.splitext(file_name)[0], thumb_extension)
thumb_extension = getattr(
settings, "WAGTAIL_VIDEOS_THUMBNAIL_EXTENSION", "jpg"
).lower()
thumb_name = "{}_thumb.{}".format(os.path.splitext(file_name)[0], thumb_extension)

try:
output_dir = tempfile.mkdtemp()
output_file = os.path.join(output_dir, thumb_name)
cmd = [
'ffmpeg',
'-v', 'quiet',
'-itsoffset', '-4',
'-i', file_path,
'-update', 'true',
'-vframes', '1',
'-an',
'-vf', 'scale=iw:-1', # Make thumbnail the size & aspect ratio of the input video
"ffmpeg",
"-v",
"quiet",
"-itsoffset",
"-4",
"-i",
file_path,
"-update",
"true",
"-vframes",
"1",
"-an",
"-vf",
"scale=iw:-1", # Make thumbnail the size & aspect ratio of the input video
output_file,
]
try:
Expand All @@ -72,7 +106,7 @@ def get_thumbnail(file_path: str) -> Optional[ContentFile]:
)
except subprocess.CalledProcessError:
return None
return ContentFile(open(output_file, 'rb').read(), thumb_name)
return ContentFile(open(output_file, "rb").read(), thumb_name)
finally:
shutil.rmtree(output_dir, ignore_errors=True)

Expand All @@ -98,7 +132,9 @@ def run(self):
)

output_file = os.path.join(output_dir, transcode_name)
ffmpeg_cmd = self._get_ffmpeg_command(input_file, output_file, media_format, self.transcode.quality)
ffmpeg_cmd = self._get_ffmpeg_command(
input_file, output_file, media_format, self.transcode.quality
)
try:
with open(os.devnull, "r") as fnull:
subprocess.check_output(
Expand Down Expand Up @@ -179,22 +215,24 @@ def _get_ffmpeg_command(
output_file,
]

def _get_quality_param(self, media_format: MediaFormats, quality: VideoQuality) -> str:
def _get_quality_param(
self, media_format: MediaFormats, quality: VideoQuality
) -> str:
if media_format is MediaFormats.WEBM:
return {
VideoQuality.LOWEST: '50',
VideoQuality.DEFAULT: '22',
VideoQuality.HIGHEST: '4'
VideoQuality.LOWEST: "50",
VideoQuality.DEFAULT: "22",
VideoQuality.HIGHEST: "4",
}[quality]
elif media_format is MediaFormats.MP4:
return {
VideoQuality.LOWEST: '28',
VideoQuality.DEFAULT: '24',
VideoQuality.HIGHEST: '18'
VideoQuality.LOWEST: "28",
VideoQuality.DEFAULT: "24",
VideoQuality.HIGHEST: "18",
}[quality]
elif media_format is MediaFormats.OGG:
return {
VideoQuality.LOWEST: '5',
VideoQuality.DEFAULT: '7',
VideoQuality.HIGHEST: '9'
VideoQuality.LOWEST: "5",
VideoQuality.DEFAULT: "7",
VideoQuality.HIGHEST: "9",
}[quality]

0 comments on commit 6e8e55e

Please sign in to comment.