refactor: move rtmp, restream into config, portal into core

This commit is contained in:
Jan Koppe 2024-02-27 20:44:35 +01:00
parent ba03d9be1a
commit 72824d32d4
Signed by: thunfisch
GPG Key ID: BE935B0735A2129B
70 changed files with 247 additions and 715 deletions

View File

@ -1,8 +1,8 @@
# Generated by Django 3.0.5 on 2020-04-26 17:25
# Generated by Django 5.0.2 on 2024-02-27 19:20
from django.db import migrations, models
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
@ -10,7 +10,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('rtmp', '0002_stream_publish_counter'),
('config', '0001_initial'),
]
operations = [
@ -18,27 +18,22 @@ class Migration(migrations.Migration):
name='Identity',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identity', models.CharField(default=uuid.uuid4, max_length=36, unique=True)),
('identity', models.UUIDField(default=uuid.uuid4, unique=True)),
('name', models.CharField(max_length=100)),
('notes', models.TextField()),
('heartbeat', models.DateTimeField(blank=True)),
('notes', models.TextField(blank=True)),
('heartbeat', models.DateTimeField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='Task',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
('type', models.CharField(max_length=100)),
('config_id', models.IntegerField()),
('configuration', models.TextField()),
('stream', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rtmp.Stream')),
],
),
migrations.CreateModel(
name='Claim',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='concierge.Identity')),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='concierge.Task')),
('claimed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='concierge.identity')),
('stream', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='config.stream')),
],
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-26 18:34
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('concierge', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='claim',
name='id',
field=models.CharField(default=uuid.uuid4, max_length=36, primary_key=True, serialize=False, unique=True),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-26 18:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('concierge', '0002_auto_20200426_1834'),
]
operations = [
migrations.AlterField(
model_name='identity',
name='heartbeat',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='identity',
name='notes',
field=models.TextField(blank=True),
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-26 19:12
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('concierge', '0003_auto_20200426_1835'),
]
operations = [
migrations.AddField(
model_name='task',
name='claimed_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='concierge.Identity'),
),
migrations.AddField(
model_name='task',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, serialize=False, unique=True),
),
migrations.DeleteModel(
name='Claim',
),
]

View File

@ -1,30 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-26 20:07
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('concierge', '0004_auto_20200426_1912'),
]
operations = [
migrations.AlterField(
model_name='identity',
name='identity',
field=models.UUIDField(default=uuid.uuid4, unique=True),
),
migrations.AlterField(
model_name='task',
name='claimed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='concierge.Identity'),
),
migrations.AlterField(
model_name='task',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, unique=True),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.0.6 on 2020-05-31 09:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('concierge', '0005_auto_20200426_2007'),
]
operations = [
migrations.AddField(
model_name='task',
name='config_id',
field=models.IntegerField(default=0),
preserve_default=False,
),
]

View File

@ -1,6 +1,6 @@
import uuid
from django.db import models
from rtmp.models import Stream
from config.models import Stream
class Identity(models.Model):

View File

@ -1,7 +1,7 @@
from django.dispatch import receiver
from rtmp.signals import stream_inactive
from .models import Task
from rtmp.models import Stream
from config.signals import stream_inactive
from config.models import Stream
@receiver(stream_inactive)

View File

@ -1,3 +0,0 @@
from django.test import TestCase # noqa
# Create your tests here.

12
source/config/admin.py Normal file
View File

@ -0,0 +1,12 @@
from django.contrib import admin
from guardian.admin import GuardedModelAdmin
from .models import Stream, Restream
@admin.register(Stream)
class StreamAdmin(GuardedModelAdmin):
fields = ['stream', 'name', 'publish_counter']
@admin.register(Restream)
class RestreamAdmin(GuardedModelAdmin):
fields = ['name', 'active', 'stream', 'format', 'target']

9
source/config/apps.py Normal file
View File

@ -0,0 +1,9 @@
from django.apps import AppConfig
class ConfigConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'config'
def ready(self):
import config.signals, config.signals_shared # noqa

View File

@ -3,9 +3,9 @@ from guardian.shortcuts import get_objects_for_user
from . import models
class RestreamConfigFilteredStreamForm(ModelForm):
class RestreamFilteredStreamForm(ModelForm):
class Meta:
model = models.RestreamConfig
model = models.Restream
fields = ['name', 'stream', 'target', 'format', 'active']
def __init__(self, *args, **kwargs):
@ -13,4 +13,4 @@ class RestreamConfigFilteredStreamForm(ModelForm):
super().__init__(*args, **kwargs)
# limit the stream selection to user-accessible streams
self.fields['stream'].queryset = get_objects_for_user(user, 'rtmp.view_stream')
self.fields['stream'].queryset = get_objects_for_user(user, 'config.view_stream')

View File

@ -0,0 +1,40 @@
# Generated by Django 5.0.2 on 2024-02-27 19:20
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Stream',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stream', models.UUIDField(default=uuid.uuid4, help_text='Stream ID for this stream', unique=True)),
('name', models.CharField(help_text='Name for this stream', max_length=100)),
('publish_counter', models.PositiveIntegerField(default=0)),
],
),
migrations.CreateModel(
name='Restream',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('target', models.CharField(help_text='restream_target_help', max_length=500)),
('name', models.CharField(help_text='restream_name_help', max_length=100)),
('active', models.BooleanField(help_text='restream_activate_help')),
('format', models.CharField(choices=[('flv', 'flv (RTMP)'), ('mpegts', 'mpegts (SRT)')], default='flv', help_text='restream_format_help', max_length=6)),
('stream', models.ForeignKey(help_text='restream_stream_help', on_delete=django.db.models.deletion.CASCADE, to='config.stream')),
],
options={
'verbose_name': 'restream_verbose_name',
'verbose_name_plural': 'restream_verbose_name_plural',
},
),
]

View File

@ -1,30 +1,16 @@
import json
import uuid
from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext as _
from django.db.models.signals import pre_delete
from portier.common import handlers
from . import signals
class Application(models.Model):
name = models.CharField(max_length=100, unique=True, help_text=_("rtmp_application_name"))
class Meta:
verbose_name = _('application_verbose_name')
verbose_name_plural = _('application_verbose_name_plural')
def class_name(self):
return _('aplication_class_name')
def __str__(self):
return self.name
from . import signals_shared
class Stream(models.Model):
application = models.ForeignKey(Application, on_delete=models.CASCADE, help_text=_('stream_application_help'))
stream = models.UUIDField(unique=True, default=uuid.uuid4, help_text=_('stream_stream_help'))
name = models.CharField(max_length=100, help_text=_('stream_name_help'))
@ -38,7 +24,7 @@ class Stream(models.Model):
# if so far there were less than one incoming streams, this stream
# is now being considered active
if self.publish_counter < 1:
signals.stream_active.send(sender=self.__class__,
signals_shared.stream_active.send(sender=self.__class__,
stream=str(self.stream),
param=param
)
@ -55,14 +41,14 @@ class Stream(models.Model):
# if we now have less than one incoming stream, this stream is being
# considered inactive
if self.publish_counter < 1:
signals.stream_inactive.send(sender=self.__class__,
signals_shared.stream_inactive.send(sender=self.__class__,
stream=str(self.stream),
param=param
)
self.save()
def get_absolute_url(self):
return reverse('rtmp:stream_detail', kwargs={'pk': self.pk})
return reverse('config:stream_detail', kwargs={'pk': self.pk})
def class_name(self):
return _('stream_class_name')
@ -71,5 +57,41 @@ class Stream(models.Model):
return self.name
pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Application)
pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Stream)
class Restream(models.Model):
FORMATS = (
('flv', 'flv (RTMP)'),
('mpegts', 'mpegts (SRT)'),
)
stream = models.ForeignKey(Stream, on_delete=models.CASCADE, help_text=_('restream_stream_help'))
target = models.CharField(max_length=500, help_text=_('restream_target_help'))
name = models.CharField(max_length=100, help_text=_('restream_name_help'))
active = models.BooleanField(help_text=_('restream_activate_help'))
format = models.CharField(max_length=6, choices=FORMATS, default='flv', help_text=_('restream_format_help'))
class Meta:
verbose_name = _('restream_verbose_name')
verbose_name_plural = _('restream_verbose_name_plural')
def class_name(self):
return _('restream_class_name')
def get_absolute_url(self):
return reverse('config:restream_detail', kwargs={'pk': self.pk})
def __str__(self):
return '{} to {}'.format(self.stream, self.name)
def get_json_config(self):
config = {
'name': self.name,
'app': settings.GLOBAL_STREAM_NAMESPACE,
'stream': str(self.stream.stream),
'target': self.target,
'format': self.format
}
return json.dumps(config)
pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Restream)

View File

@ -1,22 +1,20 @@
from django.dispatch import receiver
from django.db.models.signals import post_save, post_delete
from rtmp.signals import stream_active
from .models import RestreamConfig
from rtmp.models import Stream
from .models import Restream, Stream
from concierge.models import Task
from .signals_shared import stream_active, stream_inactive
@receiver(stream_active)
def create_tasks(sender, **kwargs):
stream = Stream.objects.get(stream=kwargs['stream'])
instances = RestreamConfig.objects.filter(active=True, stream=stream)
instances = Restream.objects.filter(active=True, stream=stream)
for instance in instances:
task = Task(stream=instance.stream, type='restream', config_id=instance.id,
configuration=instance.get_json_config())
task.save()
@receiver(post_save, sender=RestreamConfig)
@receiver(post_save, sender=Restream)
def update_tasks(sender, **kwargs):
instance = kwargs['instance']
# TODO: check for breaking changes using update_fields. This needs custom save_model functions though.
@ -35,7 +33,7 @@ def update_tasks(sender, **kwargs):
task.save()
@receiver(post_delete, sender=RestreamConfig)
@receiver(post_delete, sender=Restream)
def delete_tasks(sender, **kwargs):
instance = kwargs['instance']
# Get the current task instance if it exists, and remove it

View File

@ -1,4 +1,4 @@
from django.dispatch import Signal
stream_active = Signal()
stream_inactive = Signal()
stream_inactive = Signal()

View File

@ -9,7 +9,7 @@
<div class="col-sm border-right">
<form method="post">
{% csrf_token %}
<p>{% blocktrans with restreamconfig_config_name=object.name %}are_you_sure_you_want_to_delete_"{{ restreamconfig_config_name }}"?{% endblocktrans %}</p>
<p>{% blocktrans with restream_config_name=object.name %}are_you_sure_you_want_to_delete_"{{ restream_config_name }}"?{% endblocktrans %}</p>
{% buttons %}
<button type="submit" class="btn btn-danger" value="login">
{% fa5_icon 'trash' %} {% trans "delete" %}

View File

@ -9,15 +9,15 @@
{% block 'content' %}
<div class="row justify-content-between">
<div class="col">
<h6>{% trans "restreamconfig_configuration_details_header" %}</h6>
<h6>{% trans "restream_configuration_details_header" %}</h6>
</div>
{% get_obj_perms user for object as "obj_perms" %}
<div class="col-auto">
{% if "change_restreamconfig" in obj_perms %}
<a href="{% url 'restream:restreamconfig_change' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-primary">{% fa5_icon 'edit' %} {% trans 'change' %}</a>
{% if "change_restream" in obj_perms %}
<a href="{% url 'config:restream_change' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-primary">{% fa5_icon 'edit' %} {% trans 'change' %}</a>
{% endif %}
{% if "delete_restreamconfig" in obj_perms %}
<a href="{% url 'restream:restreamconfig_delete' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-danger">{% fa5_icon 'trash' %} {% trans 'delete' %}</a>
{% if "delete_restream" in obj_perms %}
<a href="{% url 'config:restream_delete' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-danger">{% fa5_icon 'trash' %} {% trans 'delete' %}</a>
{% endif %}
</div>
</div>
@ -28,7 +28,7 @@
<dt class="col-sm-3">{% trans "name" %}</dt>
<dd class="col-sm-9">{{ object.name }}</dd>
<dt class="col-sm-3">{% trans "stream" %}</dt>
<dd class="col-sm-9"><a href="{% url 'rtmp:stream_detail' pk=object.stream.pk %}">{{ object.stream.name }}</a></dd>
<dd class="col-sm-9"><a href="{% url 'config:stream_detail' pk=object.stream.pk %}">{{ object.stream.name }}</a></dd>
<dt class="col-sm-3">{% trans "active" %}</dt>
<dd class="col-sm-9">
{% if object.active %}

View File

@ -2,7 +2,7 @@
{% load i18n %}
{% load bootstrap4 %}
{% block 'content' %}
<h6>{% trans "update_restreamconfig_configuration_header" %}</h6>
<h6>{% trans "create_new_restream_configuration_header" %}</h6>
<hr class="my-4">
<div class="row">
<div class="col-sm border-right">
@ -18,7 +18,7 @@
</form>
</div>
<div class="col-sm">
{% trans "restreamconfig_configuration_text_html" %}
{% trans "restream_configuration_text_html" %}
</div>
</div>
{% endblock %}

View File

@ -8,12 +8,12 @@
<div class="row justify-content-between">
<div class="col">
<h6>{% trans "restreamconfig_configuration_header" %}</h6>
<h6>{% trans "restream_configuration_header" %}</h6>
</div>
<div class="col-auto">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<a href="{% url 'restream:restreamconfig_create' %}" type="button" class="btn btn-sm btn-outline-primary">{% fa5_icon 'plus' %} {% trans "create" %}</a>
<a href="{% url 'config:restream_create' %}" type="button" class="btn btn-sm btn-outline-primary">{% fa5_icon 'plus' %} {% trans "create" %}</a>
</div>
</div>
</div>
@ -59,5 +59,5 @@
<script src="{% static 'js/vue.min.js' %}"></script>
<script src="{% static 'js/axios.min.js' %}"></script>
<script src="{% static 'js/restreamconfig-list.js' %}"></script>
<script src="{% static 'js/restream-list.js' %}"></script>
{% endblock %}

View File

@ -2,7 +2,7 @@
{% load i18n %}
{% load bootstrap4 %}
{% block 'content' %}
<h6>{% trans "create_new_restreamconfig_configuration_header" %}</h6>
<h6>{% trans "update_restream_configuration_header" %}</h6>
<hr class="my-4">
<div class="row">
<div class="col-sm border-right">
@ -18,7 +18,7 @@
</form>
</div>
<div class="col-sm">
{% trans "restreamconfig_configuration_text_html" %}
{% trans "restream_configuration_text_html" %}
</div>
</div>
{% endblock %}

View File

@ -14,10 +14,10 @@
{% get_obj_perms user for object as "obj_perms" %}
<div class="col-auto">
{% if "change_stream" in obj_perms %}
<a href="{% url 'rtmp:stream_change' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-primary">{% fa5_icon 'edit' %} {% trans 'change' %}</a>
<a href="{% url 'config:stream_change' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-primary">{% fa5_icon 'edit' %} {% trans 'change' %}</a>
{% endif %}
{% if "delete_stream" in obj_perms %}
<a href="{% url 'rtmp:stream_delete' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-danger">{% fa5_icon 'trash' %} {% trans 'delete' %}</a>
<a href="{% url 'config:stream_delete' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-danger">{% fa5_icon 'trash' %} {% trans 'delete' %}</a>
{% endif %}
</div>
</div>
@ -27,8 +27,6 @@
<dl class="row">
<dt class="col-sm-3">{% trans "name" %}</dt>
<dd class="col-sm-9">{{ object.name }}</dd>
<dt class="col-sm-3">{% trans "application" %}</dt>
<dd class="col-sm-9">{{ object.application }}</dd>
</dl>
<h6>{% trans "how_to_configure_your_encoder_header" %}</h6>
<hr class="my-4">

View File

@ -13,7 +13,7 @@
<div class="col-auto">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<a href="{% url 'rtmp:stream_create' %}" type="button" class="btn btn-sm btn-outline-primary">{% fa5_icon 'plus' %} {% trans "create" %}</a>
<a href="{% url 'config:stream_create' %}" type="button" class="btn btn-sm btn-outline-primary">{% fa5_icon 'plus' %} {% trans "create" %}</a>
</div>
</div>
</div>

3
source/config/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,7 +1,7 @@
from django.urls import path
from . import views
app_name = 'rtmp'
app_name = 'config'
urlpatterns = [
path('callback/srs', views.callback_srs, name='callback_srs'),
@ -10,4 +10,9 @@ urlpatterns = [
path('streams/<int:pk>/change', views.StreamChange.as_view(), name='stream_change'),
path('streams/<int:pk>/delete', views.StreamDelete.as_view(), name='stream_delete'),
path('streams/create', views.StreamCreate.as_view(), name='stream_create'),
path('restream/', views.RestreamList.as_view(), name='restream_list'),
path('restream/<int:pk>/', views.RestreamDetail.as_view(), name='restream_detail'),
path('restream/<int:pk>/change', views.RestreamUpdate.as_view(), name='restream_change'),
path('restream/<int:pk>/delete', views.RestreamDelete.as_view(), name='restream_delete'),
path('restream/create', views.RestreamCreate.as_view(), name='restream_create'),
]

View File

@ -7,13 +7,12 @@ from django.urls import reverse_lazy
from django.contrib.auth.decorators import login_required
from django.contrib.admin.utils import NestedObjects
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from guardian.decorators import permission_required_or_403
from guardian.shortcuts import assign_perm
from . import models
from . import forms
from . import models, forms
logger = logging.getLogger(__name__)
@ -40,8 +39,7 @@ def callback_srs(request):
except KeyError:
return HttpResponse('1', status=401)
try:
application = models.Application.objects.get(name=app_name)
stream = models.Stream.objects.get(stream=stream_name, application=application)
stream = models.Stream.objects.get(stream=stream_name)
except ObjectDoesNotExist:
return HttpResponse('1', status=401)
@ -56,14 +54,14 @@ def callback_srs(request):
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('rtmp.add_stream'),
@method_decorator(permission_required_or_403('config.add_stream'),
name='dispatch')
class StreamList(ListView):
model = models.Stream
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('rtmp.view_stream',
@method_decorator(permission_required_or_403('config.view_stream',
(models.Stream, 'pk', 'pk')),
name='dispatch')
class StreamDetail(DetailView):
@ -71,12 +69,11 @@ class StreamDetail(DetailView):
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('rtmp.change_stream',
@method_decorator(permission_required_or_403('config.change_stream',
(models.Stream, 'pk', 'pk')),
name='dispatch')
class StreamChange(UpdateView):
model = models.Stream
form_class = forms.StreamFilteredApplicationForm
template_name_suffix = '_update_form'
def get_form_kwargs(self):
@ -86,16 +83,11 @@ class StreamChange(UpdateView):
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('rtmp.add_stream'),
@method_decorator(permission_required_or_403('config.add_stream'),
name='dispatch')
class StreamCreate(CreateView):
model = models.Stream
form_class = forms.StreamFilteredApplicationForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
fields = ['name']
def form_valid(self, form):
valid = super().form_valid(form)
@ -108,12 +100,12 @@ class StreamCreate(CreateView):
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('rtmp.delete_stream',
@method_decorator(permission_required_or_403('config.delete_stream',
(models.Stream, 'pk', 'pk')),
name='dispatch')
class StreamDelete(DeleteView):
model = models.Stream
success_url = reverse_lazy('rtmp:stream_list')
success_url = reverse_lazy('config:stream_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -125,3 +117,65 @@ class StreamDelete(DeleteView):
print(context['to_delete'])
return context
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('config.add_restream'),
name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class RestreamList(ListView):
model = models.Restream
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('config.view_restream',
(models.Restream, 'pk', 'pk')),
name='dispatch')
class RestreamDetail(DetailView):
model = models.Restream
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('config.change_restream',
(models.Restream, 'pk', 'pk')),
name='dispatch')
class RestreamUpdate(UpdateView):
model = models.Restream
form_class = forms.RestreamFilteredStreamForm
template_name_suffix = '_update_form'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('config.add_restream'),
name='dispatch')
class RestreamCreate(CreateView):
model = models.Restream
form_class = forms.RestreamFilteredStreamForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
valid = super().form_valid(form)
if valid:
user = self.request.user
assign_perm('view_restream', user, self.object)
assign_perm('change_restream', user, self.object)
assign_perm('delete_restream', user, self.object)
return valid
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('config.delete_restream',
(models.Restream, 'pk', 'pk')),
name='dispatch')
class RestreamDelete(DeleteView):
model = models.Restream
success_url = reverse_lazy('config:restream_list')

View File

@ -5,7 +5,7 @@ from django.conf import settings
PERMISSIONS = [
'add_stream',
'add_restreamconfig'
'add_restream'
]

5
source/core/views.py Normal file
View File

@ -0,0 +1,5 @@
from django.shortcuts import render
def index(request):
return render(request, 'core/index.html')

View File

@ -1,3 +0,0 @@
from django.contrib import admin # noqa
# Register your models here.

View File

@ -1,5 +0,0 @@
from django.apps import AppConfig
class PortalConfig(AppConfig):
name = 'portal'

View File

@ -1,3 +0,0 @@
from django.db import models # noqa
# Create your models here.

View File

@ -1,3 +0,0 @@
from django.test import TestCase # noqa
# Create your tests here.

View File

@ -1,6 +0,0 @@
from django.shortcuts import render
def index(request):
# do fancy stuff here maybe
return render(request, 'portal/index.html')

View File

@ -30,7 +30,7 @@ CSRF_TRUSTED_ORIGINS = os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", default="ht
DEFAULT_GROUP = 'default'
DEFAULT_RTMP_APPPLICATION = 'live'
GLOBAL_STREAM_NAMESPACE = 'live'
# Application definition
@ -47,10 +47,8 @@ INSTALLED_APPS = [
'bootstrap4',
'fontawesome_5',
'core.apps.CoreConfig',
'portal.apps.PortalConfig',
'rtmp.apps.RtmpConfig',
'config.apps.ConfigConfig',
'concierge.apps.ConciergeConfig',
'restream.apps.RestreamConfig',
]
MIDDLEWARE = [

View File

@ -1,18 +1,3 @@
"""portier URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
@ -21,9 +6,8 @@ urlpatterns = [
path('accounts/', include('django.contrib.auth.urls')),
path('i18n/', include('django.conf.urls.i18n')),
path('admin/', admin.site.urls),
path('rtmp/', include('rtmp.urls')),
path('restream/', include('restream.urls')),
path('config/', include('config.urls')),
path('concierge/', include('concierge.urls')),
path('api/v1/', include('restapi.urls')),
path('', include('portal.urls')),
path('', include('core.urls')),
]

View File

@ -1,13 +1,12 @@
from django.urls import path, include
from rest_framework import routers
from .views import ApplicationViewSet, StreamViewSet, RestreamConfigViewSet
from .views import StreamViewSet, RestreamViewSet
router = routers.DefaultRouter()
router.register(r'applications', ApplicationViewSet)
router.register(r'streams', StreamViewSet)
router.register(r'restreamconfigs', RestreamConfigViewSet)
router.register(r'restreams', RestreamViewSet)
app_name = 'restapi'

View File

@ -1,20 +1,7 @@
from rest_framework_guardian.serializers import ObjectPermissionsAssignmentMixin
from rest_framework import serializers, viewsets
from rest_framework_guardian import filters
from rtmp.models import Application, Stream
from restream.models import RestreamConfig
class ApplicationSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Application
fields = ['id', 'name']
class ApplicationViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Application.objects.all()
serializer_class = ApplicationSerializer
filter_backends = [filters.ObjectPermissionsFilter]
from config.models import Stream, Restream
class StreamSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer):
@ -38,9 +25,9 @@ class StreamViewSet(viewsets.ModelViewSet):
filter_backends = [filters.ObjectPermissionsFilter]
class RestreamConfigSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer):
class RestreamSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer):
class Meta:
model = RestreamConfig
model = Restream
fields = '__all__'
def get_permissions_map(self, created):
@ -53,12 +40,12 @@ class RestreamConfigSerializer(ObjectPermissionsAssignmentMixin, serializers.Mod
def validate_stream(self, value):
request = self.context['request']
if not request.user.has_perm('rtmp.view_stream', value):
if not request.user.has_perm('config.view_stream', value):
raise serializers.ValidationError('Access to stream is not authorized')
return value
class RestreamConfigViewSet(viewsets.ModelViewSet):
queryset = RestreamConfig.objects.all()
serializer_class = RestreamConfigSerializer
class RestreamViewSet(viewsets.ModelViewSet):
queryset = Restream.objects.all()
serializer_class = RestreamSerializer
filter_backends = [filters.ObjectPermissionsFilter]

View File

@ -1,10 +0,0 @@
from django.contrib import admin
from guardian.admin import GuardedModelAdmin
from .models import RestreamConfig
@admin.register(RestreamConfig)
class RestreamConfigAdmin(GuardedModelAdmin):
fields = ['name', 'active', 'stream', 'format', 'target']

View File

@ -1,8 +0,0 @@
from django.apps import AppConfig
class RestreamConfig(AppConfig):
name = 'restream'
def ready(self):
import restream.signals # noqa

View File

@ -1,26 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-23 19:04
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('rtmp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='RestreamConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('target', models.CharField(max_length=500)),
('name', models.CharField(max_length=100)),
('active', models.BooleanField()),
('stream', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rtmp.Stream')),
],
),
]

View File

@ -1,39 +0,0 @@
# Generated by Django 3.0.5 on 2020-05-01 13:02
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('rtmp', '0004_auto_20200501_1302'),
('restream', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='restreamconfig',
options={'verbose_name': 'restreamconfig_verbose_name', 'verbose_name_plural': 'restreamconfig_verbose_name_plural'},
),
migrations.AlterField(
model_name='restreamconfig',
name='active',
field=models.BooleanField(help_text='restreamconfig_activate_help'),
),
migrations.AlterField(
model_name='restreamconfig',
name='name',
field=models.CharField(help_text='restreamconfig_name_help', max_length=100),
),
migrations.AlterField(
model_name='restreamconfig',
name='stream',
field=models.ForeignKey(help_text='restreamconfig_stream_help', on_delete=django.db.models.deletion.CASCADE, to='rtmp.Stream'),
),
migrations.AlterField(
model_name='restreamconfig',
name='target',
field=models.CharField(help_text='restreamconfig_target_help', max_length=500),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.13 on 2021-12-14 17:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('restream', '0002_auto_20200501_1302'),
]
operations = [
migrations.AddField(
model_name='restreamconfig',
name='format',
field=models.CharField(choices=[('flv', 'flv (RTMP)'), ('mpegts', 'mpegts (SRT)')], default='flv', help_text='restreamconfig_format_help', max_length=6),
),
]

View File

@ -1,46 +0,0 @@
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.db.models.signals import pre_delete
from portier.common import handlers
import json
from rtmp.models import Stream
class RestreamConfig(models.Model):
FORMATS = (
('flv', 'flv (RTMP)'),
('mpegts', 'mpegts (SRT)'),
)
stream = models.ForeignKey(Stream, on_delete=models.CASCADE, help_text=_('restreamconfig_stream_help'))
target = models.CharField(max_length=500, help_text=_('restreamconfig_target_help'))
name = models.CharField(max_length=100, help_text=_('restreamconfig_name_help'))
active = models.BooleanField(help_text=_('restreamconfig_activate_help'))
format = models.CharField(max_length=6, choices=FORMATS, default='flv', help_text=_('restreamconfig_format_help'))
class Meta:
verbose_name = _('restreamconfig_verbose_name')
verbose_name_plural = _('restreamconfig_verbose_name_plural')
def class_name(self):
return _('restreamconfig_class_name')
def get_absolute_url(self):
return reverse('restream:restreamconfig_detail', kwargs={'pk': self.pk})
def __str__(self):
return '{} to {}'.format(self.stream, self.name)
def get_json_config(self):
config = {
'name': self.name,
'app': self.stream.application.name,
'stream': str(self.stream.stream),
'target': self.target,
'format': self.format
}
return json.dumps(config)
pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=RestreamConfig)

View File

@ -1,3 +0,0 @@
from django.test import TestCase # noqa
# Create your tests here.

View File

@ -1,12 +0,0 @@
from django.urls import path
from . import views
app_name = 'restream'
urlpatterns = [
path('restreamconfig/', views.RestreamConfigList.as_view(), name='restreamconfig_list'),
path('restreamconfig/<int:pk>/', views.RestreamConfigDetail.as_view(), name='restreamconfig_detail'),
path('restreamconfig/<int:pk>/change', views.RestreamConfigChange.as_view(), name='restreamconfig_change'),
path('restreamconfig/<int:pk>/delete', views.RestreamConfigDelete.as_view(), name='restreamconfig_delete'),
path('restreamconfig/create', views.RestreamConfigCreate.as_view(), name='restreamconfig_create'),
]

View File

@ -1,72 +0,0 @@
from django.urls import reverse_lazy
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import ensure_csrf_cookie
from django.utils.decorators import method_decorator
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from guardian.decorators import permission_required_or_403
from guardian.shortcuts import assign_perm
from . import models
from . import forms
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('restream.add_restreamconfig'),
name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class RestreamConfigList(ListView):
model = models.RestreamConfig
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('restream.view_restreamconfig',
(models.RestreamConfig, 'pk', 'pk')),
name='dispatch')
class RestreamConfigDetail(DetailView):
model = models.RestreamConfig
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('restream.change_restreamconfig',
(models.RestreamConfig, 'pk', 'pk')),
name='dispatch')
class RestreamConfigChange(UpdateView):
model = models.RestreamConfig
form_class = forms.RestreamConfigFilteredStreamForm
template_name_suffix = '_update_form'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('restream.add_restreamconfig'),
name='dispatch')
class RestreamConfigCreate(CreateView):
model = models.RestreamConfig
form_class = forms.RestreamConfigFilteredStreamForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
valid = super().form_valid(form)
if valid:
user = self.request.user
assign_perm('view_restreamconfig', user, self.object)
assign_perm('change_restreamconfig', user, self.object)
assign_perm('delete_restreamconfig', user, self.object)
return valid
@method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('restream.delete_restreamconfig',
(models.RestreamConfig, 'pk', 'pk')),
name='dispatch')
class RestreamConfigDelete(DeleteView):
model = models.RestreamConfig
success_url = reverse_lazy('restream:restreamconfig_list')

View File

@ -1,15 +0,0 @@
from django.contrib import admin
from guardian.admin import GuardedModelAdmin
from .models import Application, Stream
@admin.register(Application)
class ApplicationAdmin(GuardedModelAdmin):
fields = ['name']
@admin.register(Stream)
class StreamAdmin(GuardedModelAdmin):
fields = ['application', 'stream', 'name', 'publish_counter']

View File

@ -1,8 +0,0 @@
from django.apps import AppConfig
class RtmpConfig(AppConfig):
name = 'rtmp'
def ready(self):
import rtmp.signals # noqa

View File

@ -1,16 +0,0 @@
from django.forms import ModelForm
from guardian.shortcuts import get_objects_for_user
from . import models
class StreamFilteredApplicationForm(ModelForm):
class Meta:
model = models.Stream
fields = ['name', 'application']
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
# limit the stream selection to user-accessible streams
self.fields['application'].queryset = get_objects_for_user(user, 'rtmp.view_application')

View File

@ -1,16 +0,0 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group
from django.conf import settings
from guardian.shortcuts import assign_perm
from rtmp import models
class Command(BaseCommand):
help = 'Creates a default RTMP application that is available to all users in the default group'
def handle(self, *args, **options):
default_group, _ = Group.objects.get_or_create(name=settings.DEFAULT_GROUP)
default_app, _ = models.Application.objects.get_or_create(name=settings.DEFAULT_RTMP_APPPLICATION)
assign_perm('view_application', default_group, default_app)

View File

@ -1,32 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-23 19:04
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Application',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='rtmp_application_name', max_length=100, unique=True)),
],
),
migrations.CreateModel(
name='Stream',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stream', models.CharField(default=uuid.uuid4, max_length=64, unique=True)),
('name', models.CharField(max_length=100)),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rtmp.Application')),
],
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-26 17:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rtmp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='stream',
name='publish_counter',
field=models.PositiveIntegerField(default=0),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.0.5 on 2020-04-26 18:34
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('rtmp', '0002_stream_publish_counter'),
]
operations = [
migrations.AlterField(
model_name='stream',
name='stream',
field=models.UUIDField(default=uuid.uuid4, unique=True),
),
]

View File

@ -1,34 +0,0 @@
# Generated by Django 3.0.5 on 2020-05-01 13:02
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('rtmp', '0003_auto_20200426_1834'),
]
operations = [
migrations.AlterModelOptions(
name='application',
options={'verbose_name': 'application_verbose_name', 'verbose_name_plural': 'application_verbose_name_plural'},
),
migrations.AlterField(
model_name='stream',
name='application',
field=models.ForeignKey(help_text='stream_application_help', on_delete=django.db.models.deletion.CASCADE, to='rtmp.Application'),
),
migrations.AlterField(
model_name='stream',
name='name',
field=models.CharField(help_text='stream_name_help', max_length=100),
),
migrations.AlterField(
model_name='stream',
name='stream',
field=models.UUIDField(default=uuid.uuid4, help_text='stream_stream_help', unique=True),
),
]

View File

@ -1,39 +0,0 @@
# Generated by Django 3.0.6 on 2020-05-31 09:51
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('rtmp', '0004_auto_20200501_1302'),
]
operations = [
migrations.AlterModelOptions(
name='application',
options={'verbose_name': 'RTMP application', 'verbose_name_plural': 'RTMP applications'},
),
migrations.AlterField(
model_name='application',
name='name',
field=models.CharField(help_text='RTMP application name', max_length=100, unique=True),
),
migrations.AlterField(
model_name='stream',
name='application',
field=models.ForeignKey(help_text='Application which the stream is assigned to', on_delete=django.db.models.deletion.CASCADE, to='rtmp.Application'),
),
migrations.AlterField(
model_name='stream',
name='name',
field=models.CharField(help_text='Name for this stream', max_length=100),
),
migrations.AlterField(
model_name='stream',
name='stream',
field=models.UUIDField(default=uuid.uuid4, help_text='Stream ID for this stream', unique=True),
),
]

View File

@ -1,3 +0,0 @@
from django.test import TestCase # noqa
# Create your tests here.

View File

@ -25,7 +25,6 @@ migrate() {
initialize() {
python manage.py createdefaultgroup
python manage.py createdefaultapplication
}
wait_for_redis

View File

@ -13,7 +13,7 @@ var app = new Vue({
},
toggleActive(cfg) {
axios
.patch('/api/v1/restreamconfigs/' + cfg.id + '/', { active: !cfg.active })
.patch('/api/v1/restream/' + cfg.id + '/', { active: !cfg.active })
.then(response => {
i = this.cfgs.findIndex((obj => obj.id == cfg.id))
Vue.set(this.cfgs, i, response.data)
@ -22,7 +22,7 @@ var app = new Vue({
},
fetchData() {
axios
.get('/api/v1/restreamconfigs/')
.get('/api/v1/restreams/')
.then(response => {
this.cfgs = response.data
this.isLoading = false

View File

@ -39,8 +39,8 @@
{% trans "navbar_streaming" %}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarStreamingDropdown">
<a class="dropdown-item{% if not perms.rtmp.add_stream %} disabled{% endif %}" href="{% url 'rtmp:stream_list' %}">{% trans "navbar_configuration_streams" %}</a>
<a class="dropdown-item{% if not perms.restream.add_restreamconfig %} disabled{% endif %}" href="{% url 'restream:restreamconfig_list' %}">{% trans "navbar_configuration_restreams" %}</a>
<a class="dropdown-item{% if not perms.config.add_stream %} disabled{% endif %}" href="{% url 'config:stream_list' %}">{% trans "navbar_configuration_streams" %}</a>
<a class="dropdown-item{% if not perms.config.add_restream %} disabled{% endif %}" href="{% url 'config:restream_list' %}">{% trans "navbar_configuration_restreams" %}</a>
</div>
</li>
{% endif %}