Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implemented HLS stream source as new plugin type. #84

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hls_source_url = models.CharField(
hls_source_url = models.URLField(

Should this be a URLField? Can the max_length fall back to Django's default?

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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an extra space here after the hls.min.js.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could that source be configurable and default to the cdn? Alternatively, please add a comment to the README on how to overwrite this template.

<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',);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log('manifest loaded, found ' + data.levels.length + ' quality level',);

Is this left over from debugging?

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
Loading