Skip to content

Commit

Permalink
feat: Implemented HLS stream source as new plugin type.
Browse files Browse the repository at this point in the history
Also implemented show_controls and autoplay attributes on the
video element. These are particularly useful when showing a
live stream.
  • Loading branch information
raffael-mnhn committed Nov 30, 2024
1 parent 22fef2c commit b83f60b
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 3 deletions.
31 changes: 30 additions & 1 deletion djangocms_video/cms_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class VideoPlayerPlugin(CMSPluginBase):
name = _('Video player')
text_enabled = True
allow_children = True
child_classes = ['VideoSourcePlugin', 'VideoTrackPlugin']
child_classes = ['VideoSourcePlugin', 'VideoTrackPlugin', 'HlsStreamSourcePlugin']
form = forms.VideoPlayerPluginForm

fieldsets = [
Expand All @@ -32,13 +32,17 @@ class VideoPlayerPlugin(CMSPluginBase):
'fields': (
'poster',
'attributes',
'show_controls',
'autoplay',
)
})
]

def render(self, context, instance, placeholder):
context = super().render(context, instance, placeholder)
context['video_template'] = instance.template
context['show_controls'] = instance.show_controls
context['autoplay'] = instance.autoplay
return context

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


class HlsStreamSourcePlugin(CMSPluginBase):
model = models.HlsStreamSource
name = _('HLS Stream Source')
module = _('Video player')
require_parent = True
parent_classes = ['VideoPlayerPlugin']

fieldsets = [
(None, {
'fields': (
'hls_source_url',
)
}),
]

def render(self, context, instance, placeholder):
context = super().render(context, instance, placeholder)
context['source_id'] = instance.id
return context

def get_render_template(self, context, instance, placeholder):
return 'djangocms_video/{}/hls_stream_source.html'.format(context.get('video_template', 'default'))


class VideoTrackPlugin(CMSPluginBase):
model = models.VideoTrack
name = _('Track')
Expand Down Expand Up @@ -101,5 +129,6 @@ def get_render_template(self, context, instance, placeholder):


plugin_pool.register_plugin(VideoPlayerPlugin)
plugin_pool.register_plugin(HlsStreamSourcePlugin)
plugin_pool.register_plugin(VideoSourcePlugin)
plugin_pool.register_plugin(VideoTrackPlugin)
23 changes: 23 additions & 0 deletions djangocms_video/migrations/0012_hlsstreamsource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.0.9 on 2024-11-30 13:11

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('cms', '0035_auto_20230822_2208_squashed_0036_auto_20240311_1028'),
('djangocms_video', '0011_alter_videoplayer_cmsplugin_ptr_and_more'),
]

operations = [
migrations.CreateModel(
name='HlsStreamSource',
fields=[
('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')),
('hls_source_url', models.CharField(max_length=1024, verbose_name='HLS Source URL')),
],
bases=('cms.cmsplugin',),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.0.9 on 2024-11-30 14:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('djangocms_video', '0012_hlsstreamsource'),
]

operations = [
migrations.AddField(
model_name='videoplayer',
name='autoplay',
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'),
),
migrations.AddField(
model_name='videoplayer',
name='show_controls',
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'),
),
]
27 changes: 27 additions & 0 deletions djangocms_video/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ class VideoPlayer(CMSPlugin):
verbose_name=_('Attributes'),
blank=True,
)
show_controls = models.BooleanField(
verbose_name=_('Show controls'),
default=True,
help_text=_(
'If enabled, the video will be shown with Play, Pause and Seek '
'elements that allow the user to control playback.'
),
)
autoplay = models.BooleanField(
verbose_name=_('Autoplay'),
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.'
),
)

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


class HlsStreamSource(CMSPlugin):
"""
Renders the HTML <source> element inside of <video> for an HLS stream defined by a .m3u8 URL.
"""
hls_source_url = models.CharField(
verbose_name=_('HLS Source URL'),
max_length=1024,
)


class VideoTrack(CMSPlugin):
"""
Renders the HTML <track> element inside <video>.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{% load i18n cms_tags sekizai_tags %}

{% if not disabled %}
{% with instance.hls_source_url as url %}
<source id="{{ source_id }}" src="{{ url }}" type="application/x-mpegURL" {{ instance.attributes_str }}>
{% endwith %}
{% endif %}

{% addtoblock "js" %}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js "></script>
<script>
// Find first source ending in a .m3u8 path and return its url
function getHlsUrl(videoElement) {
const sources = videoElement.querySelectorAll("source");
for (source of sources) {
if (source.src.endsWith("m3u8")) {
return source.src;
}
}
return null;
}

function attachHlsStream(sourceElement) {
if (Hls.isSupported()) {
let video = sourceElement.parentElement;
let hls = new Hls();
let hlsUrl = getHlsUrl(video);
hls.loadSource(hlsUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
console.log('manifest loaded, found ' + data.levels.length + ' quality level',);
video.play();
});

hls.on(Hls.Events.ERROR, function (event, data) {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.MEDIA_ERROR:
console.error('fatal media error encountered, try to recover');
hls.recoverMediaError();
break;
case Hls.ErrorTypes.NETWORK_ERROR:
console.error('fatal network error encountered', data);
break;
default:
hls.destroy();
break;
}
}
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = hlsUrl;
video.addEventListener('loadedmetadata', () => {
video.play();
});
} else {
console.error('HLS is not supported on this browser.');
}
}

attachHlsStream(document.getElementById('{{ source_id }}'));

</script>
{% endaddtoblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
{% endwith %}
{% else %}
{# render <source> or <track> plugins #}
<video controls {{ instance.attributes_str }}
{% if instance.poster %} poster="{{ instance.poster.url }}"{% endif %}>
<video {% if show_controls %} controls {% endif %}
{% if autoplay %} autoplay {% endif %}
{{ instance.attributes_str }}
{% if instance.poster %} poster="{{ instance.poster.url }}"{% endif %}>
{% for plugin in instance.child_plugin_instances %}
{% render_plugin plugin %}
{% endfor %}
Expand Down

0 comments on commit b83f60b

Please sign in to comment.