Merge branch subtitles
Implement subtitles support through the <track> tag
This commit is contained in:
@@ -34,6 +34,13 @@ def create(model: Type[T], **kwargs) -> T:
|
||||
if model is models.Upload:
|
||||
return _create_with_defaults(models.Upload, kwargs, file=SimpleUploadedFile('some_file.txt', b'some contents'))
|
||||
|
||||
if model is models.Track:
|
||||
return _create_with_defaults(models.Track, kwargs,
|
||||
video=lambda: create(models.Video),
|
||||
lang='en',
|
||||
upload=lambda: create(models.Upload)
|
||||
)
|
||||
|
||||
raise NotImplementedError('Factory for %s not implemented' % model)
|
||||
|
||||
|
||||
|
||||
96
tests/videodinges/models/test_track.py
Normal file
96
tests/videodinges/models/test_track.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from tests.videodinges import factories, UploadMixin
|
||||
from videodinges.models import Track, Video, Upload
|
||||
|
||||
|
||||
class TrackTestCase(UploadMixin, TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.video = Video.objects.create(title='Title', slug='slug', description='Description')
|
||||
|
||||
def test_model_is_created_with_required_fields(self):
|
||||
Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload))
|
||||
track = Track.objects.all()[0]
|
||||
self.assertEqual(track.video.slug, 'slug')
|
||||
self.assertEqual(track.default, False)
|
||||
self.assertEqual(track.kind, 'subtitles')
|
||||
self.assertEqual(track.lang, 'en')
|
||||
self.assertEqual(track.label, None)
|
||||
self.assertEqual(track.upload.file.name, 'some_file.txt')
|
||||
|
||||
def test_model_is_created_with_nonrequired_fields(self):
|
||||
Track.objects.create(
|
||||
video=self.video,
|
||||
lang='en',
|
||||
upload=factories.create(Upload),
|
||||
default=True,
|
||||
label='Something',
|
||||
kind='chapters',
|
||||
)
|
||||
track = Track.objects.all()[0]
|
||||
self.assertEqual(track.video.slug, 'slug')
|
||||
self.assertEqual(track.default, True)
|
||||
self.assertEqual(track.kind, 'chapters')
|
||||
self.assertEqual(track.lang, 'en')
|
||||
self.assertEqual(track.label, 'Something')
|
||||
self.assertEqual(track.upload.file.name, 'some_file.txt')
|
||||
|
||||
def test_can_create_two_models(self):
|
||||
model1 = Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload))
|
||||
model2 = Track.objects.create(video=self.video, lang='nl', upload=factories.create(Upload))
|
||||
self.assertEqual({model1, model2}, set((m for m in Track.objects.all())))
|
||||
|
||||
self.assertEqual(model1.video, self.video)
|
||||
self.assertEqual(model1.default, False)
|
||||
self.assertEqual(model1.kind, 'subtitles')
|
||||
self.assertEqual(model1.lang, 'en')
|
||||
|
||||
self.assertEqual(model2.video, self.video)
|
||||
self.assertEqual(model2.default, False)
|
||||
self.assertEqual(model2.kind, 'subtitles')
|
||||
self.assertEqual(model2.lang, 'nl')
|
||||
|
||||
def test_can_create_two_models_with_one_default(self):
|
||||
model1 = Track.objects.create(video=self.video, default=True, lang='en', upload=factories.create(Upload))
|
||||
model2 = Track.objects.create(video=self.video, lang='nl', upload=factories.create(Upload))
|
||||
self.assertEqual({model1, model2}, set((m for m in Track.objects.all())))
|
||||
|
||||
self.assertEqual(model1.video, self.video)
|
||||
self.assertEqual(model1.default, True)
|
||||
self.assertEqual(model1.kind, 'subtitles')
|
||||
self.assertEqual(model1.lang, 'en')
|
||||
|
||||
self.assertEqual(model2.video, self.video)
|
||||
self.assertEqual(model2.default, False)
|
||||
self.assertEqual(model2.kind, 'subtitles')
|
||||
self.assertEqual(model2.lang, 'nl')
|
||||
|
||||
def test_cannot_set_default_twice(self):
|
||||
video = factories.create(Video)
|
||||
Track.objects.create(video=video, default=True, lang='en', upload=factories.create(Upload))
|
||||
with self.assertRaisesMessage(IntegrityError, 'UNIQUE constraint failed: tracks.video_id'):
|
||||
Track.objects.create(video=video, default=True, lang='nl', upload=factories.create(Upload))
|
||||
|
||||
def test_cannot_set_two_subtitles_with_same_lang(self):
|
||||
video = factories.create(Video)
|
||||
Track.objects.create(video=video, lang='en', upload=factories.create(Upload))
|
||||
with self.assertRaisesMessage(IntegrityError, 'UNIQUE constraint failed: tracks.video_id'):
|
||||
Track.objects.create(video=video, lang='en', upload=factories.create(Upload))
|
||||
|
||||
def test_can_create_two_models_with_same_lang_but_different_kind(self):
|
||||
model1 = Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload))
|
||||
model2 = Track.objects.create(video=self.video, lang='en', kind='chapters', upload=factories.create(Upload))
|
||||
self.assertEqual({model1, model2}, set((m for m in Track.objects.all())))
|
||||
|
||||
self.assertEqual(model1.video, self.video)
|
||||
self.assertEqual(model1.default, False)
|
||||
self.assertEqual(model1.kind, 'subtitles')
|
||||
self.assertEqual(model1.lang, 'en')
|
||||
|
||||
self.assertEqual(model2.video, self.video)
|
||||
self.assertEqual(model2.default, False)
|
||||
self.assertEqual(model2.kind, 'chapters')
|
||||
self.assertEqual(model2.lang, 'en')
|
||||
@@ -8,6 +8,7 @@ from videodinges.models import Transcoding, Video, qualities, transcoding_types,
|
||||
|
||||
class TranscodingTestCase(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
video = Video.objects.create(title='Title', slug='slug', description='Description')
|
||||
Transcoding.objects.create(video=video, quality=qualities[0].name, type=str(transcoding_types[0]), url='https://some_url')
|
||||
|
||||
|
||||
@@ -246,3 +246,191 @@ class VideoTestCase(UploadMixin, TestCase):
|
||||
)
|
||||
|
||||
|
||||
class VideoWithTrackTestCase(UploadMixin, TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client = Client()
|
||||
self.video = factories.create(
|
||||
models.Video,
|
||||
title='Vid 1',
|
||||
slug='vid-1',
|
||||
default_quality='480p',
|
||||
)
|
||||
factories.create(
|
||||
models.Transcoding,
|
||||
video=self.video,
|
||||
quality='480p',
|
||||
type='video/webm; codecs="vp9, opus"',
|
||||
url='http://480p.webm',
|
||||
)
|
||||
factories.create(
|
||||
models.Transcoding,
|
||||
video=self.video,
|
||||
quality='480p',
|
||||
type='video/mp4',
|
||||
url='http://480p.mp4',
|
||||
)
|
||||
|
||||
def test_video_view_renders_track_properly(self):
|
||||
|
||||
factories.create(
|
||||
models.Track,
|
||||
video=self.video,
|
||||
lang='en',
|
||||
)
|
||||
|
||||
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
content:str = resp.content.decode(resp.charset)
|
||||
|
||||
self.assertInHTML(
|
||||
"""<video width="853" height="480" controls="controls">
|
||||
<source src="http://480p.mp4" type='video/mp4' />
|
||||
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||
<track src="/uploads/some_file.txt" srclang="en" kind="subtitles" label="en" />
|
||||
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||
</video>""",
|
||||
content,
|
||||
)
|
||||
|
||||
def test_video_view_renders_multiple_tracks_properly(self):
|
||||
|
||||
track_en = factories.create(
|
||||
models.Track,
|
||||
video=self.video,
|
||||
lang='en',
|
||||
)
|
||||
|
||||
track_nl = factories.create(
|
||||
models.Track,
|
||||
video=self.video,
|
||||
lang='nl',
|
||||
)
|
||||
|
||||
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
content:str = resp.content.decode(resp.charset)
|
||||
|
||||
self.assertInHTML(
|
||||
f"""<video width="853" height="480" controls="controls">
|
||||
<source src="http://480p.mp4" type='video/mp4' />
|
||||
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||
<track src="{ track_en.upload.file.url }" srclang="en" kind="subtitles" label="en" />
|
||||
<track src="{ track_nl.upload.file.url }" srclang="nl" kind="subtitles" label="nl" />
|
||||
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||
</video>""",
|
||||
content,
|
||||
)
|
||||
|
||||
|
||||
def test_video_view_renders_default_track_properly(self):
|
||||
|
||||
factories.create(
|
||||
models.Track,
|
||||
video=self.video,
|
||||
lang='en',
|
||||
default=True
|
||||
)
|
||||
|
||||
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
content:str = resp.content.decode(resp.charset)
|
||||
|
||||
self.assertInHTML(
|
||||
"""<video width="853" height="480" controls="controls">
|
||||
<source src="http://480p.mp4" type='video/mp4' />
|
||||
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||
<track src="/uploads/some_file.txt" default="default" srclang="en" kind="subtitles" label="en" />
|
||||
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||
</video>""",
|
||||
content,
|
||||
)
|
||||
|
||||
def test_video_view_renders_multiple_tracks_properly_with_one_default(self):
|
||||
|
||||
track_en = factories.create(
|
||||
models.Track,
|
||||
video=self.video,
|
||||
lang='en',
|
||||
default=True
|
||||
)
|
||||
|
||||
track_nl = factories.create(
|
||||
models.Track,
|
||||
video=self.video,
|
||||
lang='nl',
|
||||
)
|
||||
|
||||
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
content:str = resp.content.decode(resp.charset)
|
||||
|
||||
self.assertInHTML(
|
||||
f"""<video width="853" height="480" controls="controls">
|
||||
<source src="http://480p.mp4" type='video/mp4' />
|
||||
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||
<track src="{ track_en.upload.file.url }" default="default" srclang="en" kind="subtitles" label="en" />
|
||||
<track src="{ track_nl.upload.file.url }" srclang="nl" kind="subtitles" label="nl" />
|
||||
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||
</video>""",
|
||||
content,
|
||||
)
|
||||
|
||||
def test_video_view_renders_track_label(self):
|
||||
|
||||
factories.create(
|
||||
models.Track,
|
||||
video=self.video,
|
||||
lang='en',
|
||||
label='Some Label',
|
||||
)
|
||||
|
||||
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
content:str = resp.content.decode(resp.charset)
|
||||
|
||||
self.assertInHTML(
|
||||
"""<video width="853" height="480" controls="controls">
|
||||
<source src="http://480p.mp4" type='video/mp4' />
|
||||
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||
<track src="/uploads/some_file.txt" srclang="en" kind="subtitles" label="Some Label" />
|
||||
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||
</video>""",
|
||||
content,
|
||||
)
|
||||
|
||||
|
||||
def test_video_view_renders_track_kind(self):
|
||||
|
||||
factories.create(
|
||||
models.Track,
|
||||
video=self.video,
|
||||
lang='en',
|
||||
kind='captions',
|
||||
)
|
||||
|
||||
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
content:str = resp.content.decode(resp.charset)
|
||||
|
||||
self.assertInHTML(
|
||||
"""<video width="853" height="480" controls="controls">
|
||||
<source src="http://480p.mp4" type='video/mp4' />
|
||||
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||
<track src="/uploads/some_file.txt" srclang="en" kind="captions" label="en" />
|
||||
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||
</video>""",
|
||||
content,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Iterable
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -17,16 +19,37 @@ class TranscodingsForm(forms.ModelForm):
|
||||
self.add_error('upload', validation_error)
|
||||
return cleaned_data
|
||||
|
||||
class TrackInlineFormset(forms.BaseInlineFormSet):
|
||||
forms: Iterable[forms.ModelForm]
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
default_cnt = 0
|
||||
for form in self.forms:
|
||||
try:
|
||||
if form.cleaned_data['default'] is True:
|
||||
default_cnt += 1
|
||||
except AttributeError:
|
||||
pass
|
||||
if default_cnt > 1:
|
||||
form.add_error('default', ValidationError('Can set only one track as default'))
|
||||
return cleaned_data
|
||||
|
||||
class TranscodingsInline(admin.StackedInline):
|
||||
model = models.Transcoding
|
||||
form = TranscodingsForm
|
||||
fields = ['quality', 'type', 'url', 'upload']
|
||||
extra = 0
|
||||
|
||||
class TracksInline(admin.StackedInline):
|
||||
model = models.Track
|
||||
formset = TrackInlineFormset
|
||||
fields = ('default', 'kind', 'lang', 'label', 'upload')
|
||||
extra = 0
|
||||
|
||||
class VideoAdmin(admin.ModelAdmin):
|
||||
model = models.Video
|
||||
fields = ['title', 'description', 'slug', 'poster', 'og_image', 'created_at', 'default_quality']
|
||||
inlines = [TranscodingsInline]
|
||||
inlines = (TranscodingsInline, TracksInline)
|
||||
list_display = ('title', 'slug', 'created_at', 'updated_at')
|
||||
ordering = ('-created_at', )
|
||||
|
||||
|
||||
@@ -108,6 +108,33 @@ class Transcoding(models.Model):
|
||||
]
|
||||
db_table = 'transcodings'
|
||||
|
||||
class Track(models.Model):
|
||||
KINDS = (
|
||||
'subtitles',
|
||||
'captions',
|
||||
'descriptions',
|
||||
'chapters',
|
||||
'metadata',
|
||||
)
|
||||
id = models.AutoField(primary_key=True)
|
||||
video = models.ForeignKey(Video, on_delete=models.CASCADE, related_name='tracks')
|
||||
default = models.BooleanField(default=False)
|
||||
kind = models.CharField(choices=((kind, kind) for kind in KINDS), max_length=128, default=KINDS[0])
|
||||
lang = models.CharField(max_length=128)
|
||||
label = models.CharField(max_length=128, blank=True, null=True)
|
||||
upload = models.OneToOneField(Upload, on_delete=models.PROTECT)
|
||||
|
||||
def __str__(self):
|
||||
return '%s_%s' % (self.kind, self.lang)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('video', 'kind', 'lang')
|
||||
constraints = (
|
||||
constraints.UniqueConstraint(
|
||||
fields=('video',), condition=Q(default=True), name='only_one_default_per_video'),
|
||||
)
|
||||
db_table = 'tracks'
|
||||
|
||||
def get_quality_by_name(name: str) -> Optional[Quality]:
|
||||
for quality in qualities:
|
||||
if quality.name == name:
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<video width="{{ width }}" height="{{ height }}" {% if poster %}poster="{{ poster }}" {% endif %}controls="controls">
|
||||
{% for source in sources %}
|
||||
<source src="{{ source.src }}" type='{{ source.type|safe }}' />
|
||||
{% endfor %}
|
||||
{% for track in tracks %}
|
||||
<track{% if track.default %} default="default"{% endif %} src="{{ track.src }}" srclang="{{ track.srclang }}" kind="{{ track.kind }}" label="{{ track.label }}" />
|
||||
{% endfor %}
|
||||
You need a browser that understands HTML5 video and supports {% for i in used_codecs %}{{ i }}{% if not loop.last %} or {% endif %}{% endfor %} codecs.
|
||||
</video><br />
|
||||
|
||||
@@ -51,6 +51,16 @@ def video(request: HttpRequest, slug: str) -> HttpResponse:
|
||||
|
||||
template_data['qualities'] = qualities.keys()
|
||||
|
||||
template_data['tracks'] = [
|
||||
{
|
||||
'default': track.default,
|
||||
'src': track.upload.file.url,
|
||||
'srclang': track.lang,
|
||||
'kind': track.kind,
|
||||
'label': track.label or track.lang,
|
||||
} for track in video.tracks.all()
|
||||
]
|
||||
|
||||
return render(request, 'video.html.j2', template_data, using='jinja2')
|
||||
|
||||
def index(request: HttpRequest) -> HttpResponse:
|
||||
|
||||
Reference in New Issue
Block a user