Skip to content

Commit 605e6ad

Browse files
committed
Implemented HLS stream source as new plugin type.
Also implemented show_controls and autoplay attributes on the video element. These are particularly useful when showing a live stream.
1 parent 22fef2c commit 605e6ad

File tree

6 files changed

+171
-3
lines changed

6 files changed

+171
-3
lines changed

djangocms_video/cms_plugins.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class VideoPlayerPlugin(CMSPluginBase):
1010
name = _('Video player')
1111
text_enabled = True
1212
allow_children = True
13-
child_classes = ['VideoSourcePlugin', 'VideoTrackPlugin']
13+
child_classes = ['VideoSourcePlugin', 'VideoTrackPlugin', 'HlsStreamSourcePlugin']
1414
form = forms.VideoPlayerPluginForm
1515

1616
fieldsets = [
@@ -32,13 +32,17 @@ class VideoPlayerPlugin(CMSPluginBase):
3232
'fields': (
3333
'poster',
3434
'attributes',
35+
'show_controls',
36+
'autoplay',
3537
)
3638
})
3739
]
3840

3941
def render(self, context, instance, placeholder):
4042
context = super().render(context, instance, placeholder)
4143
context['video_template'] = instance.template
44+
context['show_controls'] = instance.show_controls
45+
context['autoplay'] = instance.autoplay
4246
return context
4347

4448
def get_render_template(self, context, instance, placeholder):
@@ -72,6 +76,30 @@ def get_render_template(self, context, instance, placeholder):
7276
return 'djangocms_video/{}/source.html'.format(context.get('video_template', 'default'))
7377

7478

79+
class HlsStreamSourcePlugin(CMSPluginBase):
80+
model = models.HlsStreamSource
81+
name = _('HLS Stream Source')
82+
module = _('Video player')
83+
require_parent = True
84+
parent_classes = ['VideoPlayerPlugin']
85+
86+
fieldsets = [
87+
(None, {
88+
'fields': (
89+
'hls_source_url',
90+
)
91+
}),
92+
]
93+
94+
def render(self, context, instance, placeholder):
95+
context = super().render(context, instance, placeholder)
96+
context['source_id'] = instance.id
97+
return context
98+
99+
def get_render_template(self, context, instance, placeholder):
100+
return 'djangocms_video/{}/hls_stream_source.html'.format(context.get('video_template', 'default'))
101+
102+
75103
class VideoTrackPlugin(CMSPluginBase):
76104
model = models.VideoTrack
77105
name = _('Track')
@@ -101,5 +129,6 @@ def get_render_template(self, context, instance, placeholder):
101129

102130

103131
plugin_pool.register_plugin(VideoPlayerPlugin)
132+
plugin_pool.register_plugin(HlsStreamSourcePlugin)
104133
plugin_pool.register_plugin(VideoSourcePlugin)
105134
plugin_pool.register_plugin(VideoTrackPlugin)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.0.9 on 2024-11-30 13:11
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('cms', '0035_auto_20230822_2208_squashed_0036_auto_20240311_1028'),
11+
('djangocms_video', '0011_alter_videoplayer_cmsplugin_ptr_and_more'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='HlsStreamSource',
17+
fields=[
18+
('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='%(app_label)s_%(class)s', serialize=False, to='cms.cmsplugin')),
19+
('hls_source_url', models.CharField(max_length=1024, verbose_name='HLS Source URL')),
20+
],
21+
bases=('cms.cmsplugin',),
22+
),
23+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.0.9 on 2024-11-30 14:56
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('djangocms_video', '0012_hlsstreamsource'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='videoplayer',
15+
name='autoplay',
16+
field=models.BooleanField(default=False, help_text='If enabled, the video will automatically play once the page is loaded. This might not work depending on how the user has configured their browser.', verbose_name='Autoplay'),
17+
),
18+
migrations.AddField(
19+
model_name='videoplayer',
20+
name='show_controls',
21+
field=models.BooleanField(default=True, help_text='If enabled, the video will be shown with Play, Pause and Seek elements that allow the user to control playback.', verbose_name='Show controls'),
22+
),
23+
]

djangocms_video/models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,23 @@ class VideoPlayer(CMSPlugin):
8080
verbose_name=_('Attributes'),
8181
blank=True,
8282
)
83+
show_controls = models.BooleanField(
84+
verbose_name=_('Show controls'),
85+
default=True,
86+
help_text=_(
87+
'If enabled, the video will be shown with Play, Pause and Seek '
88+
'elements that allow the user to control playback.'
89+
),
90+
)
91+
autoplay = models.BooleanField(
92+
verbose_name=_('Autoplay'),
93+
default=False,
94+
help_text=_(
95+
'If enabled, the video will automatically play once the page is '
96+
'loaded. This might not work depending on how the user has '
97+
'configured their browser.'
98+
),
99+
)
83100

84101
# Add an app namespace to related_name to avoid field name clashes
85102
# with any other plugins that have a field with the same name as the
@@ -164,6 +181,16 @@ def copy_relations(self, oldinstance):
164181
self.source_file = oldinstance.source_file
165182

166183

184+
class HlsStreamSource(CMSPlugin):
185+
"""
186+
Renders the HTML <source> element inside of <video> for an HLS stream defined by a .m3u8 URL.
187+
"""
188+
hls_source_url = models.CharField(
189+
verbose_name=_('HLS Source URL'),
190+
max_length=1024,
191+
)
192+
193+
167194
class VideoTrack(CMSPlugin):
168195
"""
169196
Renders the HTML <track> element inside <video>.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{% load i18n cms_tags sekizai_tags %}
2+
3+
{% if not disabled %}
4+
{% with instance.hls_source_url as url %}
5+
<source id="{{ source_id }}" src="{{ url }}" type="application/x-mpegURL" {{ instance.attributes_str }}>
6+
{% endwith %}
7+
{% endif %}
8+
9+
{% addtoblock "js" %}
10+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js "></script>
11+
<script>
12+
// Find first source ending in a .m3u8 path and return its url
13+
function getHlsUrl(videoElement) {
14+
const sources = videoElement.querySelectorAll("source");
15+
for (source of sources) {
16+
if (source.src.endsWith("m3u8")) {
17+
return source.src;
18+
}
19+
}
20+
return null;
21+
}
22+
23+
function attachHlsStream(sourceElement) {
24+
if (Hls.isSupported()) {
25+
let video = sourceElement.parentElement;
26+
let hls = new Hls();
27+
let hlsUrl = getHlsUrl(video);
28+
hls.loadSource(hlsUrl);
29+
hls.attachMedia(video);
30+
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
31+
console.log('manifest loaded, found ' + data.levels.length + ' quality level',);
32+
video.play();
33+
});
34+
35+
hls.on(Hls.Events.ERROR, function (event, data) {
36+
if (data.fatal) {
37+
switch (data.type) {
38+
case Hls.ErrorTypes.MEDIA_ERROR:
39+
console.error('fatal media error encountered, try to recover');
40+
hls.recoverMediaError();
41+
break;
42+
case Hls.ErrorTypes.NETWORK_ERROR:
43+
console.error('fatal network error encountered', data);
44+
break;
45+
default:
46+
hls.destroy();
47+
break;
48+
}
49+
}
50+
});
51+
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
52+
video.src = hlsUrl;
53+
video.addEventListener('loadedmetadata', () => {
54+
video.play();
55+
});
56+
} else {
57+
console.error('HLS is not supported on this browser.');
58+
}
59+
}
60+
61+
attachHlsStream(document.getElementById('{{ source_id }}'));
62+
63+
</script>
64+
{% endaddtoblock %}

djangocms_video/templates/djangocms_video/default/video_player.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
{% endwith %}
1313
{% else %}
1414
{# render <source> or <track> plugins #}
15-
<video controls {{ instance.attributes_str }}
16-
{% if instance.poster %} poster="{{ instance.poster.url }}"{% endif %}>
15+
<video {% if show_controls %} controls {% endif %}
16+
{% if autoplay %} autoplay {% endif %}
17+
{{ instance.attributes_str }}
18+
{% if instance.poster %} poster="{{ instance.poster.url }}"{% endif %}>
1719
{% for plugin in instance.child_plugin_instances %}
1820
{% render_plugin plugin %}
1921
{% endfor %}

0 commit comments

Comments
 (0)