From 0a857a194ceb2b197ca6bb39acca3f60643344bb Mon Sep 17 00:00:00 2001 From: Jan Koppe Date: Mon, 22 Apr 2024 14:25:15 +0200 Subject: [PATCH] wip --- source/config/admin.py | 25 +++- source/config/api/__init__.py | 1 + source/config/{api.py => api/api.py_} | 75 +++++++++-- source/config/api/pull.py | 77 +++++++++++ source/config/api/recorder.py | 65 +++++++++ source/config/api/restream.py | 78 +++++++++++ source/config/api/stream.py | 73 ++++++++++ ...odingprofile_alter_stream_name_and_more.py | 59 +++++++++ ...dingstorage_s3recordingstorage_recorder.py | 53 ++++++++ source/config/models.py | 125 +++++++++++++++++- .../management/commands/createdefaultgroup.py | 3 +- source/portier/api.py | 14 +- 12 files changed, 621 insertions(+), 27 deletions(-) create mode 100644 source/config/api/__init__.py rename source/config/{api.py => api/api.py_} (72%) create mode 100644 source/config/api/pull.py create mode 100644 source/config/api/recorder.py create mode 100644 source/config/api/restream.py create mode 100644 source/config/api/stream.py create mode 100644 source/config/migrations/0005_transcodingprofile_alter_stream_name_and_more.py create mode 100644 source/config/migrations/0006_localrecordingstorage_s3recordingstorage_recorder.py diff --git a/source/config/admin.py b/source/config/admin.py index 3d029e3..fa40d5f 100644 --- a/source/config/admin.py +++ b/source/config/admin.py @@ -1,23 +1,38 @@ from django.contrib import admin from guardian.admin import GuardedModelAdmin -from config.models import Stream, Restream, Pull, SRSNode, SRSStreamInstance +from config.models import TranscodingProfile, Stream, Restream, Pull, Recorder, LocalRecordingStorage, S3RecordingStorage, SRSNode, SRSStreamInstance +@admin.register(LocalRecordingStorage) +class LocalRecordingStorageAdmin(GuardedModelAdmin): + pass + +@admin.register(S3RecordingStorage) +class S3RecordingStorageAdmin(GuardedModelAdmin): + pass + +@admin.register(Recorder) +class RecorderAdmin(GuardedModelAdmin): + pass + +@admin.register(TranscodingProfile) +class TranscodingProfileAdmin(GuardedModelAdmin): + pass @admin.register(Stream) class StreamAdmin(GuardedModelAdmin): - fields = ['stream', 'name', 'publish_counter'] + pass @admin.register(Restream) class RestreamAdmin(GuardedModelAdmin): - fields = ['name', 'active', 'stream', 'format', 'target'] + pass @admin.register(Pull) class PullAdmin(GuardedModelAdmin): - fields = ['name', 'active', 'stream', 'source'] + pass @admin.register(SRSNode) class SRSNodeAdmin(GuardedModelAdmin): - fields = ['name', 'api_base', 'rtmp_base', 'active'] + pass @admin.register(SRSStreamInstance) class SRSStreamInstanceAdmin(GuardedModelAdmin): diff --git a/source/config/api/__init__.py b/source/config/api/__init__.py new file mode 100644 index 0000000..26a8e56 --- /dev/null +++ b/source/config/api/__init__.py @@ -0,0 +1 @@ +from . import pull, recorder, restream, stream diff --git a/source/config/api.py b/source/config/api/api.py_ similarity index 72% rename from source/config/api.py rename to source/config/api/api.py_ index e5f1d36..9bb1434 100644 --- a/source/config/api.py +++ b/source/config/api/api.py_ @@ -57,6 +57,20 @@ class StreamCreate(ModelSchema): extra = "forbid" +class LocalRecordingStorage(ModelSchema): + class Meta: + model = models.LocalRecordingStorage + fields = "__all__" + + +class LocalRecordingStoragePatch(ModelSchema): + class Meta: + model = models.LocalRecordingStorage + exclude = ["id"] + fields_optional = "__all__" + extra = "forbid" + + @router.get('/streams', response=List[Stream]) def list_streams(request): return get_objects_for_user(request.user, 'view_stream', models.Stream.objects.all()) @@ -123,7 +137,7 @@ def create_restream(request, payload: Restream): @router.get('/restreams/{id}', response=Restream) def get_restream(request, id: int): restream = get_object_or_404(models.Restream, id=id) - + if not request.user.has_perm('view_restream', restream): raise HttpError(401, "unauthorized") @@ -139,7 +153,7 @@ def update_restream(request, id: int, payload: RestreamPatch): if payload.stream: payload.stream = get_object_or_404(models.Stream, id=payload.stream) - + if not request.user.has_perm('view_stream', payload.stream): raise HttpError(401, "unauthorized") @@ -171,20 +185,20 @@ def create_pull(request, payload: Pull): raise HttpError(401, "unauthorized") pull = models.Pull.objects.create(**payload.dict()) - assign_perm( 'view_pull', request.user, Pull) - assign_perm('change_pull', request.user, Pull) - assign_perm('delete_pull', request.user, Pull) - return pull + assign_perm( 'view_pull', request.user, pull) + assign_perm('change_pull', request.user, pull) + assign_perm('delete_pull', request.user, pull) + return pull @router.get('/pulls/{id}', response=Pull) def get_pull(request, id: int): pull = get_object_or_404(models.Pull, id=id) - + if not request.user.has_perm('view_pull', pull): raise HttpError(401, "unauthorized") - return pull + return pull @router.patch('/pulls/{id}', response=Pull) @@ -196,7 +210,7 @@ def patch_pull(request, id: int, payload: PullPatch): if payload.stream: payload.stream = get_object_or_404(models.Stream, id=payload.stream) - + if not request.user.has_perm('view_stream', payload.stream): raise HttpError(401, "unauthorized") @@ -213,4 +227,45 @@ def delete_pull(request, id: int): if not request.user.has_perm('delete_pull', pull): raise HttpError(401, "unauthorized") - pull.delete() \ No newline at end of file + pull.delete() + +@router.get('/recording/storage/local', response=List[LocalRecordingStorage]) +def list_local_recording_storage(request): + return get_objects_for_user(request.user, 'view_localrecordingstorage', models.LocalRecordingStorage.objects.all()) + +@router.get('/recording/storage/local/{id}', response=LocalRecordingStorage) +def get_local_recording_storage(request, id: int): + obj = get_object_or_404(models.LocalRecordingStorage, id=id) + + if not request.user.has_perm('view_localrecordingstorage', obj): + raise HttpError(401, "unauthorized") + + return obj + +@router.post('/recording/storage/local', response=LocalRecordingStorage) +def create_local_recording_storage(request, payload: LocalRecordingStorage): + obj = models.LocalRecordingStorage.objects.create(**payload.dict()) + + assign_perm( 'view_localrecordingstorage', request.user, obj) + assign_perm('change_localrecordingstorage', request.user, obj) + assign_perm('delete_localrecordingstorage', request.user, obj) + return obj + +@router.patch('/recording/storage/local/{id}', response=LocalRecordingStorage) +def patch_local_recording_storage(request, id: int, payload: LocalRecordingStoragePatch): + obj = get_object_or_404(models.LocalRecordingStorage, id=id) + + if not request.user.has_perm('change_localrecordingstorage', obj): + raise HttpError(401, "unauthorized") + + for key, value in payload.dict(exclude_unset=True).items(): + setattr(obj, key, value) + obj.save() + return obj + +@router.delete('/recording/storage/local/{id}', response=None) +def delete_local_recording_storage(request, id: int): + obj = get_object_or_404(models.LocalRecordingStorage, id=id) + if not request.user.has_perm('delete_localrecordingstorage', obj): + raise HttpError(401, "unauthorized") + obj.delete() diff --git a/source/config/api/pull.py b/source/config/api/pull.py new file mode 100644 index 0000000..4c597c9 --- /dev/null +++ b/source/config/api/pull.py @@ -0,0 +1,77 @@ +from ninja import Router, ModelSchema, Schema +from ninja.errors import HttpError +from config import models +from typing import List +from guardian.shortcuts import get_objects_for_user, assign_perm +from django.shortcuts import get_object_or_404 + +router = Router() + +class Pull(ModelSchema): + class Meta: + model = models.Pull + fields = "__all__" + + +class PullPatch(ModelSchema): + class Meta: + model = models.Pull + exclude = ["id"] + fields_optional = "__all__" + extra = "forbid" + + +@router.get('/pulls', response=List[Pull]) +def list_pulls(request): + return get_objects_for_user(request.user, 'view_pull', models.Pull.objects.all()) + + +@router.post('/pulls', response=Pull) +def create_pull(request, payload: Pull): + if not request.user.has_perm('view_stream', payload.stream): + raise HttpError(401, "unauthorized") + + pull = models.Pull.objects.create(**payload.dict()) + assign_perm( 'view_pull', request.user, pull) + assign_perm('change_pull', request.user, pull) + assign_perm('delete_pull', request.user, pull) + return pull + + +@router.get('/pulls/{id}', response=Pull) +def get_pull(request, id: int): + pull = get_object_or_404(models.Pull, id=id) + + if not request.user.has_perm('view_pull', pull): + raise HttpError(401, "unauthorized") + + return pull + + +@router.patch('/pulls/{id}', response=Pull) +def patch_pull(request, id: int, payload: PullPatch): + pull = get_object_or_404(models.Pull, id=id) + + if not request.user.has_perm('change_pull', pull): + raise HttpError(401, "unauthorized") + + if payload.stream: + payload.stream = get_object_or_404(models.Stream, id=payload.stream) + + if not request.user.has_perm('view_stream', payload.stream): + raise HttpError(401, "unauthorized") + + for key, value in payload.dict(exclude_unset=True).items(): + setattr(pull, key, value) + pull.save() + return pull + + +@router.delete('/pulls/{id}', response=None) +def delete_pull(request, id: int): + pull = get_object_or_404(models.Pull, id=id) + + if not request.user.has_perm('delete_pull', pull): + raise HttpError(401, "unauthorized") + + pull.delete() diff --git a/source/config/api/recorder.py b/source/config/api/recorder.py new file mode 100644 index 0000000..4a2848e --- /dev/null +++ b/source/config/api/recorder.py @@ -0,0 +1,65 @@ + +from ninja import Router, ModelSchema, Schema +from ninja.errors import HttpError +from config import models +from typing import List +from guardian.shortcuts import get_objects_for_user, assign_perm +from django.shortcuts import get_object_or_404 + +router = Router() + + +class LocalRecordingStorage(ModelSchema): + class Meta: + model = models.LocalRecordingStorage + fields = "__all__" + + +class LocalRecordingStoragePatch(ModelSchema): + class Meta: + model = models.LocalRecordingStorage + exclude = ["id"] + fields_optional = "__all__" + extra = "forbid" + + +@router.get('/storage/local', response=List[LocalRecordingStorage]) +def list_local_recording_storage(request): + return get_objects_for_user(request.user, 'view_localrecordingstorage', models.LocalRecordingStorage.objects.all()) + +@router.get('/storage/local/{id}', response=LocalRecordingStorage) +def get_local_recording_storage(request, id: int): + obj = get_object_or_404(models.LocalRecordingStorage, id=id) + + if not request.user.has_perm('view_localrecordingstorage', obj): + raise HttpError(401, "unauthorized") + + return obj + +@router.post('/storage/local', response=LocalRecordingStorage) +def create_local_recording_storage(request, payload: LocalRecordingStorage): + obj = models.LocalRecordingStorage.objects.create(**payload.dict()) + + assign_perm( 'view_localrecordingstorage', request.user, obj) + assign_perm('change_localrecordingstorage', request.user, obj) + assign_perm('delete_localrecordingstorage', request.user, obj) + return obj + +@router.patch('/storage/local/{id}', response=LocalRecordingStorage) +def patch_local_recording_storage(request, id: int, payload: LocalRecordingStoragePatch): + obj = get_object_or_404(models.LocalRecordingStorage, id=id) + + if not request.user.has_perm('change_localrecordingstorage', obj): + raise HttpError(401, "unauthorized") + + for key, value in payload.dict(exclude_unset=True).items(): + setattr(obj, key, value) + obj.save() + return obj + +@router.delete('/storage/local/{id}', response=None) +def delete_local_recording_storage(request, id: int): + obj = get_object_or_404(models.LocalRecordingStorage, id=id) + if not request.user.has_perm('delete_localrecordingstorage', obj): + raise HttpError(401, "unauthorized") + obj.delete() diff --git a/source/config/api/restream.py b/source/config/api/restream.py new file mode 100644 index 0000000..9836a32 --- /dev/null +++ b/source/config/api/restream.py @@ -0,0 +1,78 @@ +from ninja import Router, ModelSchema, Schema +from ninja.errors import HttpError +from config import models +from typing import List +from guardian.shortcuts import get_objects_for_user, assign_perm +from django.shortcuts import get_object_or_404 + +router = Router() + + +class Restream(ModelSchema): + class Meta: + model = models.Restream + fields = "__all__" + + +class RestreamPatch(ModelSchema): + class Meta: + model = models.Restream + exclude = ["id"] + fields_optional = "__all__" + extra = "forbid" + + +@router.get('/restreams', response=List[Restream]) +def list_restreams(request): + return get_objects_for_user(request.user, 'view_restream', models.Restream.objects.all()) + + +@router.post('/restreams', response=Restream) +def create_restream(request, payload: Restream): + if not request.user.has_perm('view_stream', payload.stream): + raise HttpError(401, "unauthorized") + + restream = models.Restream.objects.create(**payload.dict()) + assign_perm('view_restream', request.user, restream) + assign_perm('change_restream', request.user, restream) + assign_perm('delete_restream', request.user, restream) + return restream + + +@router.get('/restreams/{id}', response=Restream) +def get_restream(request, id: int): + restream = get_object_or_404(models.Restream, id=id) + + if not request.user.has_perm('view_restream', restream): + raise HttpError(401, "unauthorized") + + return restream + + +@router.patch('/restreams/{id}', response=Restream) +def update_restream(request, id: int, payload: RestreamPatch): + restream = get_object_or_404(models.Restream, id=id) + + if not request.user.has_perm('change_restream', restream): + raise HttpError(401, "unauthorized") + + if payload.stream: + payload.stream = get_object_or_404(models.Stream, id=payload.stream) + + if not request.user.has_perm('view_stream', payload.stream): + raise HttpError(401, "unauthorized") + + for key, value in payload.dict(exclude_unset=True).items(): + setattr(restream, key, value) + restream.save() + return restream + + +@router.delete('/restreams/{id}', response=None) +def delete_restream(request, id: int): + restream = get_object_or_404(models.Restream, id=id) + + if not request.user.has_perm('delete_restream', restream): + raise HttpError(401, "unauthorized") + + restream.delete() diff --git a/source/config/api/stream.py b/source/config/api/stream.py new file mode 100644 index 0000000..ffd5c79 --- /dev/null +++ b/source/config/api/stream.py @@ -0,0 +1,73 @@ +from ninja import Router, ModelSchema, Schema +from ninja.errors import HttpError +from config import models +from typing import List +from guardian.shortcuts import get_objects_for_user, assign_perm +from django.shortcuts import get_object_or_404 + +router = Router() + +class Stream(ModelSchema): + class Meta: + model = models.Stream + fields = "__all__" + + +class StreamPatch(ModelSchema): + class Meta: + model = models.Stream + fields = ["name"] + fields_optional = "__all__" + extra = "forbid" + + +class StreamCreate(ModelSchema): + class Meta: + model = models.Stream + fields = ["name"] + extra = "forbid" + + +@router.get('/streams', response=List[Stream]) +def list_streams(request): + return get_objects_for_user(request.user, 'view_stream', models.Stream.objects.all()) + + +@router.post('/streams', response=Stream) +def create_stream(request, payload: StreamCreate): + stream = models.Stream.objects.create(**payload.dict()) + assign_perm('view_stream', request.user, stream) + assign_perm('change_stream', request.user, stream) + assign_perm('delete_stream', request.user, stream) + return stream + + +@router.get('/streams/{id}', response=Stream) +def get_stream(request, id: int): + stream = get_object_or_404(models.Stream, id=id) + + if not request.user.has_perm('view_stream', stream): + raise HttpError(401, "unauthorized") + return stream + + +@router.patch('/streams/{id}', response=Stream) +def update_stream(request, id: int, payload: StreamPatch): + stream = get_object_or_404(models.Stream, id=id) + + if not request.user.has_perm('change_stream', stream): + raise HttpError(401, "unauthorized") + + stream.name = payload.name + stream.save() + return stream + + +@router.delete('/streams/{id}', response=None) +def delete_stream(request, id: int): + stream = get_object_or_404(models.Stream, id=id) + + if not request.user.has_perm('delete_stream', stream): + raise HttpError(401, "unauthorized") + + stream.delete() diff --git a/source/config/migrations/0005_transcodingprofile_alter_stream_name_and_more.py b/source/config/migrations/0005_transcodingprofile_alter_stream_name_and_more.py new file mode 100644 index 0000000..37c5420 --- /dev/null +++ b/source/config/migrations/0005_transcodingprofile_alter_stream_name_and_more.py @@ -0,0 +1,59 @@ +# Generated by Django 5.0.2 on 2024-04-01 18:42 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0004_alter_pull_source_alter_restream_target'), + ] + + operations = [ + migrations.CreateModel( + name='TranscodingProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='transcodingprofile_name_help', max_length=100)), + ('video_map_stream', models.PositiveIntegerField(default=0, help_text='transcodingprofile_video_map_stream_help')), + ('video_codec', models.CharField(choices=[('copy', 'Copy'), ('h264', 'H.264'), ('h265', 'H.265')], help_text='transcodingprofile_video_codec_help', max_length=4)), + ('video_bitrate', models.PositiveIntegerField(default=6000, help_text='transcodingprofile_video_bitrate_help')), + ('video_frame_rate', models.PositiveIntegerField(default=30, help_text='transcodingprofile_video_frame_rate_help')), + ('video_resolution', models.CharField(help_text='transcodingprofile_video_resolution_help', max_length=9)), + ('video_gop_size', models.PositiveIntegerField(default=60, help_text='transcodingprofile_video_gop_size_help')), + ('video_pixel_format', models.CharField(choices=[('yuv420', 'YUV420'), ('yuv422', 'YUV422'), ('yuv444', 'YUV444')], help_text='transcodingprofile_video_pixel_format_help', max_length=10)), + ('video_bitrate_mode', models.CharField(choices=[('cbr', 'CBR'), ('vbr', 'VBR')], help_text='transcodingprofile_video_bitrate_mode_help', max_length=10)), + ('audio_map_stream', models.PositiveIntegerField(default=0, help_text='transcodingprofile_audio_map_stream_help')), + ('audio_codec', models.CharField(choices=[('copy', 'Copy'), ('aac', 'AAC')], help_text='transcodingprofile_audio_codec_help', max_length=10)), + ('audio_bitrate', models.PositiveIntegerField(default=160, help_text='transcodingprofile_audio_bitrate_help')), + ('audio_channels', models.PositiveIntegerField(default=2, help_text='transcodingprofile_audio_channels_help')), + ('audio_sample_rate', models.PositiveIntegerField(default=48000, help_text='transcodingprofile_audio_sample_rate_help')), + ], + options={ + 'verbose_name': 'transcodingprofile_verbose_name', + 'verbose_name_plural': 'transcodingprofile_verbose_name_plural', + }, + ), + 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), + ), + migrations.AddField( + model_name='pull', + name='transcodingprofile', + field=models.ForeignKey(blank=True, help_text='restream_transcodingprofile_help', null=True, on_delete=django.db.models.deletion.PROTECT, to='config.transcodingprofile'), + ), + migrations.AddField( + model_name='restream', + name='transcodingprofile', + field=models.ForeignKey(blank=True, help_text='restream_transcodingprofile_help', null=True, on_delete=django.db.models.deletion.PROTECT, to='config.transcodingprofile'), + ), + ] diff --git a/source/config/migrations/0006_localrecordingstorage_s3recordingstorage_recorder.py b/source/config/migrations/0006_localrecordingstorage_s3recordingstorage_recorder.py new file mode 100644 index 0000000..7270fee --- /dev/null +++ b/source/config/migrations/0006_localrecordingstorage_s3recordingstorage_recorder.py @@ -0,0 +1,53 @@ +# Generated by Django 5.0.2 on 2024-04-13 08:52 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0005_transcodingprofile_alter_stream_name_and_more'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='LocalRecordingStorage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='recordingstorage_name_help', max_length=100)), + ('path', models.CharField(help_text='localrecordingstorage_path_help', max_length=500)), + ], + options={ + 'verbose_name': 'localrecordingstorage_verbose_name', + 'verbose_name_plural': 'localrecordingstorage_verbose_name_plural', + }, + ), + migrations.CreateModel( + name='S3RecordingStorage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='recordingstorage_name_help', max_length=100)), + ('bucket', models.CharField(help_text='s3recordingstorage_bucket_help', max_length=100)), + ('access_key', models.CharField(help_text='s3recordingstorage_access_key_help', max_length=100)), + ('secret_key', models.CharField(help_text='s3recordingstorage_secret_key_help', max_length=100)), + ('region', models.CharField(help_text='s3recordingstorage_region_help', max_length=100)), + ], + options={ + 'verbose_name': 's3recordingstorage_verbose_name', + 'verbose_name_plural': 's3recordingstorage_verbose_name_plural', + }, + ), + migrations.CreateModel( + name='Recorder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='recorder_name_help', max_length=100)), + ('active', models.BooleanField(help_text='recorder_activate_help')), + ('storage_config_id', models.PositiveIntegerField()), + ('storage_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'config'), ('model', 'LocalRecordingStorage')), models.Q(('app_label', 'app'), ('model', 'S3RecordingStorage')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('stream', models.ForeignKey(help_text='recorder_stream_help', on_delete=django.db.models.deletion.CASCADE, to='config.stream')), + ], + ), + ] diff --git a/source/config/models.py b/source/config/models.py index 3840ee2..d797981 100644 --- a/source/config/models.py +++ b/source/config/models.py @@ -1,6 +1,9 @@ import json import uuid + +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.conf import settings from django.db import models from django.urls import reverse @@ -10,6 +13,70 @@ from portier.common import handlers from config import signals_shared from config.util import validate_stream_url +class TranscodingProfile(models.Model): + VIDEO_CODECS = ( + ('copy', 'Copy'), + ('h264', 'H.264'), + ('h265', 'H.265'), + ) + AUDIO_CODECS = ( + ('copy', 'Copy'), + ('aac', 'AAC'), + ) + VIDEO_PIXEL_FORMATS = ( + ('yuv420', 'YUV420'), + ('yuv422', 'YUV422'), + ('yuv444', 'YUV444'), + ) + VIDEO_BITRATE_MODES = ( + ('cbr', 'CBR'), + ('vbr', 'VBR'), + ) + name = models.CharField(max_length=100, help_text=_('transcodingprofile_name_help')) + + video_map_stream = models.PositiveIntegerField(default=0, help_text=_('transcodingprofile_video_map_stream_help')) + video_codec = models.CharField(max_length=4, choices=VIDEO_CODECS, help_text=_('transcodingprofile_video_codec_help')) + video_bitrate = models.PositiveIntegerField(default=6000, help_text=_('transcodingprofile_video_bitrate_help')) + video_frame_rate = models.PositiveIntegerField(default=30, help_text=_('transcodingprofile_video_frame_rate_help')) + video_resolution = models.CharField(max_length=9, help_text=_('transcodingprofile_video_resolution_help')) + video_gop_size = models.PositiveIntegerField(default=60, help_text=_('transcodingprofile_video_gop_size_help')) + video_pixel_format = models.CharField(max_length=10, choices=VIDEO_PIXEL_FORMATS, help_text=_('transcodingprofile_video_pixel_format_help')) + video_bitrate_mode = models.CharField(max_length=10, choices=VIDEO_BITRATE_MODES, help_text=_('transcodingprofile_video_bitrate_mode_help')) + + audio_map_stream = models.PositiveIntegerField(default=0, help_text=_('transcodingprofile_audio_map_stream_help')) + audio_codec = models.CharField(max_length=10, choices=AUDIO_CODECS, help_text=_('transcodingprofile_audio_codec_help')) + audio_bitrate = models.PositiveIntegerField(default=160, help_text=_('transcodingprofile_audio_bitrate_help')) + audio_channels = models.PositiveIntegerField(default=2, help_text=_('transcodingprofile_audio_channels_help')) + audio_sample_rate = models.PositiveIntegerField(default=48000, help_text=_('transcodingprofile_audio_sample_rate_help')) + + class Meta: + verbose_name = _('transcodingprofile_verbose_name') + verbose_name_plural = _('transcodingprofile_verbose_name_plural') + + def class_name(self): + return _('transcodingprofile_class_name') + + def get_absolute_url(self): + return reverse('config:transcodingprofile_detail', kwargs={'pk': self.pk}) + + def __str__(self): + return str(self.name) + + def get_concierge_configuration(self): + return { + 'config_version': 1, + 'video_map_stream': self.video_map_stream, + 'video_codec': self.video_codec, + 'video_bitrate': self.video_bitrate, + 'video_frame_rate': self.video_frame_rate, + 'video_resolution': self.video_resolution, + 'audio_map_stream': self.audio_map_stream, + 'audio_codec': self.audio_codec, + 'audio_bitrate': self.audio_bitrate, + 'audio_channels': self.audio_channels, + 'audio_sample_rate': self.audio_sample_rate, + } + class Stream(models.Model): stream = models.UUIDField(unique=True, default=uuid.uuid4, help_text=_('stream_stream_help')) @@ -45,9 +112,6 @@ class Stream(models.Model): return str(self.name) -pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Stream) - - class SRSNode(models.Model): name = models.CharField(max_length=100, help_text=_('srsnode_name_help')) api_base = models.CharField(max_length=256, help_text=_('srsnode_api_base_help')) @@ -81,6 +145,7 @@ class Pull(models.Model): source = models.CharField(max_length=500, validators=[validate_stream_url], help_text=_('pull_source_help')) active = models.BooleanField(help_text=_('pull_activate_help')) name = models.CharField(max_length=100, help_text=_('pull_name_help')) + transcodingprofile = models.ForeignKey(TranscodingProfile, null=True, blank=True, on_delete=models.PROTECT, help_text=_('restream_transcodingprofile_help')) class Meta: verbose_name = _('pull_verbose_name') @@ -116,17 +181,16 @@ class Pull(models.Model): node = SRSNode.objects.filter(active=True).order_by('?').first() return { + 'config_version': 1, "type": "pull", "active": self.active, "name": self.name, "source": self.source, "target": f"{node.rtmp_base}/{settings.GLOBAL_STREAM_NAMESPACE}/{self.stream.stream}", + 'transcoding_profile': self.transcodingprofile.get_concierge_configuration(), } -pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Pull) - - class Restream(models.Model): FORMATS = ( ('flv', 'flv (RTMP)'), @@ -137,6 +201,7 @@ class Restream(models.Model): 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')) + transcodingprofile = models.ForeignKey(TranscodingProfile, null=True, blank=True, on_delete=models.PROTECT, help_text=_('restream_transcodingprofile_help')) class Meta: verbose_name = _('restream_verbose_name') @@ -176,7 +241,53 @@ class Restream(models.Model): 'stream_source_url': f"{rtmp_base}/{settings.GLOBAL_STREAM_NAMESPACE}/{self.stream.stream}", 'stream_target_url': self.target, 'stream_target_transport': self.format, + 'transcoding_profile': self.transcodingprofile.get_concierge_configuration(), } - +pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Stream) pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Restream) +pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Pull) + + +### Recording Storage configurations + +class RecordingStorage(models.Model): + name = models.CharField(max_length=100, help_text=_('recordingstorage_name_help')) + + class Meta: + abstract = True + + +class LocalRecordingStorage(RecordingStorage): + path = models.CharField(max_length=500, help_text=_('localrecordingstorage_path_help')) + + class Meta: + verbose_name = _('localrecordingstorage_verbose_name') + verbose_name_plural = _('localrecordingstorage_verbose_name_plural') + + def __str__(self): + return self.name + +class S3RecordingStorage(RecordingStorage): + bucket = models.CharField(max_length=100, help_text=_('s3recordingstorage_bucket_help')) + access_key = models.CharField(max_length=100, help_text=_('s3recordingstorage_access_key_help')) + secret_key = models.CharField(max_length=100, help_text=_('s3recordingstorage_secret_key_help')) + region = models.CharField(max_length=100, help_text=_('s3recordingstorage_region_help')) + + class Meta: + verbose_name = _('s3recordingstorage_verbose_name') + verbose_name_plural = _('s3recordingstorage_verbose_name_plural') + + def __str__(self): + return self.name + + +class Recorder(models.Model): + storage_type_models = models.Q(app_label = 'config', model = 'LocalRecordingStorage') | models.Q(app_label = 'app', model = 'S3RecordingStorage') + + stream = models.ForeignKey(Stream, on_delete=models.CASCADE, help_text=_('recorder_stream_help')) + name = models.CharField(max_length=100, help_text=_('recorder_name_help')) + active = models.BooleanField(help_text=_('recorder_activate_help')) + storage_type = models.ForeignKey(ContentType, limit_choices_to=storage_type_models, on_delete=models.CASCADE) + storage_config_id = models.PositiveIntegerField() + storage_config = GenericForeignKey('storage_type', 'storage_config_id') diff --git a/source/core/management/commands/createdefaultgroup.py b/source/core/management/commands/createdefaultgroup.py index abc4c01..c3dfc33 100644 --- a/source/core/management/commands/createdefaultgroup.py +++ b/source/core/management/commands/createdefaultgroup.py @@ -5,7 +5,8 @@ from django.conf import settings PERMISSIONS = [ 'add_stream', - 'add_restream' + 'add_restream', + 'add_pull', ] diff --git a/source/portier/api.py b/source/portier/api.py index 9a4182d..7c27dfc 100644 --- a/source/portier/api.py +++ b/source/portier/api.py @@ -2,7 +2,10 @@ from ninja import NinjaAPI, ModelSchema, Router from ninja.security import django_auth from django.contrib.auth.models import User -from config.api import router as config_router +import config.api as config_api +#from config.api import router as config_router +#from config.api.recorder import router as config_recorder_router +#from config.api.recorder import router as config_recorder_router from concierge.api import router as concierge_router core_router = Router() @@ -25,6 +28,9 @@ api = NinjaAPI( csrf=False, # Disable CSRF for now ) -api.add_router("/", core_router, auth=django_auth, tags=["Core"]) -api.add_router("/config/", config_router, auth=django_auth, tags=["Configuration"]) -api.add_router("/concierge/", concierge_router, auth=None, tags=["Concierge"]) +api.add_router("/", core_router, auth=django_auth, tags=["Core API"]) +api.add_router("/config/recorder/", config_api.recorder.router, auth=django_auth, tags=["Recorder Configuration API"]) +api.add_router("/config/pull/", config_api.pull.router, auth=django_auth, tags=["Pull Configuration API"]) +api.add_router("/config/stream/", config_api.stream.router, auth=django_auth, tags=["Stream Configuration API"]) +api.add_router("/config/restream/", config_api.restream.router, auth=django_auth, tags=["Resteam Configuration API"]) +api.add_router("/concierge/", concierge_router, auth=None, tags=["Concierge API"])