Skip to content

Commit

Permalink
[DONE] Opencast with Extron SMP (#1104)
Browse files Browse the repository at this point in the history
* use Opencast with Smp

* pydoc + refacto light

* flake
  • Loading branch information
mattbild authored Apr 29, 2024
1 parent 3698912 commit d5f3f99
Show file tree
Hide file tree
Showing 16 changed files with 1,930 additions and 379 deletions.
85 changes: 56 additions & 29 deletions pod/live/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@


def set_default_site(sender, **kwargs):
"""Set a default Site for Building not having one."""
from pod.live.models import Building
from django.contrib.sites.models import Site

Expand All @@ -20,40 +21,66 @@ def set_default_site(sender, **kwargs):
build.save()


def add_default_opencast(sender, **kwargs):
"""Add the key 'use_opencast' with value False, in the json conf of the Broadcaster if not present."""
from pod.live.models import Broadcaster

brds = Broadcaster.objects.filter(
piloting_implementation="SMP", piloting_conf__isnull=False
).all()
for brd in brds:
conf = brd.piloting_conf
if "use_opencast" not in conf:
brd.piloting_conf = conf.replace("}", ',"use_opencast":"false"}')
brd.save()


def save_previous_data(sender, **kwargs):
"""Save all live events if model has date and time in different fields."""
results = []
try:
with connection.cursor() as c:
c.execute("SELECT id, start_date, start_time, end_time FROM live_event")
results = c.fetchall()
for res in results:
__EVENT_DATA__["%s" % res[0]] = [res[1], res[2], res[3]]
except Exception: # OperationalError or MySQLdb.ProgrammingError
pass # print('OperationalError: ', oe)


def send_previous_data(sender, **kwargs):
"""Set start and end dates with date + time to all saved events."""
from .models import Event

for data_id in __EVENT_DATA__:
try:
evt = Event.objects.get(id=data_id)
d_start = datetime.combine(
__EVENT_DATA__[data_id][0], __EVENT_DATA__[data_id][1]
)
d_start = timezone.make_aware(d_start)
evt.start_date = d_start
d_fin = datetime.combine(
__EVENT_DATA__[data_id][0], __EVENT_DATA__[data_id][2]
)
d_fin = timezone.make_aware(d_fin)
evt.end_date = d_fin
evt.save()
except Event.DoesNotExist:
print("Event not found: %s" % data_id)


class LiveConfig(AppConfig):
"""Config file for live app."""

name = "pod.live"
default_auto_field = "django.db.models.BigAutoField"
# event_data = {}
verbose_name = _("Lives")

def ready(self):
"""Init tasks."""
pre_migrate.connect(save_previous_data, sender=self)
post_migrate.connect(add_default_opencast, sender=self)
post_migrate.connect(set_default_site, sender=self)
pre_migrate.connect(self.save_previous_data, sender=self)
post_migrate.connect(self.send_previous_data, sender=self)

def save_previous_data(self, sender, **kwargs):
results = []
try:
with connection.cursor() as c:
c.execute("SELECT id, start_date, start_time, end_time FROM live_event")
results = c.fetchall()
for res in results:
__EVENT_DATA__["%s" % res[0]] = [res[1], res[2], res[3]]
except Exception: # OperationalError or MySQLdb.ProgrammingError
pass # print('OperationalError: ', oe)

def send_previous_data(self, sender, **kwargs):
from .models import Event

for id in __EVENT_DATA__:
try:
evt = Event.objects.get(id=id)
d_start = datetime.combine(__EVENT_DATA__[id][0], __EVENT_DATA__[id][1])
d_start = timezone.make_aware(d_start)
evt.start_date = d_start
d_fin = datetime.combine(__EVENT_DATA__[id][0], __EVENT_DATA__[id][2])
d_fin = timezone.make_aware(d_fin)
evt.end_date = d_fin
evt.save()
except Event.DoesNotExist:
print("Event not found: %s" % id)
post_migrate.connect(send_previous_data, sender=self)
4 changes: 2 additions & 2 deletions pod/live/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,15 +188,15 @@ class Broadcaster(models.Model):

piloting_implementation = models.CharField(
max_length=100,
blank=True,
null=True,
default="",
verbose_name=_("Piloting implementation"),
help_text=_("Select the piloting implementation for to this broadcaster."),
)

piloting_conf = models.TextField(
null=True,
blank=True,
default="",
verbose_name=_("Piloting configuration parameters"),
help_text=_("Add piloting configuration parameters in Json format."),
)
Expand Down
104 changes: 70 additions & 34 deletions pod/live/pilotingInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import re
from abc import ABC as __ABC__, abstractmethod
from datetime import timedelta
from typing import Optional, List
from typing import Optional

import paramiko
import requests
Expand All @@ -19,22 +19,31 @@

DEFAULT_EVENT_PATH = getattr(settings, "DEFAULT_EVENT_PATH", "")

CREATE_VIDEO_FROM_FTP = "fetch file from remote using ftp"
CREATE_VIDEO_FROM_FS = "file is in Pod file system"
CREATE_VIDEO_OPENCAST = "file is automatically sent to the recorder module"

logger = logging.getLogger(__name__)

__EXISTING_BROADCASTER_IMPLEMENTATIONS__ = ["Wowza", "SMP"]

__MANDATORY_PARAMETERS__ = {
"Wowza": {"server_url", "application", "livestream"},
"SMP": {
"SMP": [
"server_url",
"sftp_port",
"user",
"password",
"record_dir_path",
"use_opencast",
"rtmp_streamer_id",
},
"record_dir_path",
],
}

__OPTIONAL_IF_OPENCAST__ = [
"record_dir_path",
]


class PilotingInterface(__ABC__):
"""Class to be implemented for any device (with an Api) we want to control in the event's page."""
Expand All @@ -51,8 +60,8 @@ def __init__(self, broadcaster: Broadcaster):
raise NotImplementedError

@abstractmethod
def copy_file_needed(self) -> bool:
"""If the video file needs to be copied from a remote server."""
def video_creation_method(self):
"""The method used to create the video."""
raise NotImplementedError

@abstractmethod
Expand Down Expand Up @@ -84,7 +93,7 @@ def is_recording(self, with_file_check=False) -> bool:
raise NotImplementedError

@abstractmethod
def start_recording(self, event_id, login=None) -> bool:
def start_recording(self, event_id) -> bool:
"""Start the recording."""
raise NotImplementedError

Expand Down Expand Up @@ -135,16 +144,25 @@ def ajax_get_mandatory_parameters(request):
impl_name = request.GET.get("impl_name", None)
params = get_mandatory_parameters(impl_name)
params_json = {}
for value in params:
params_json[value] = "..."
for key in params:
if key in __OPTIONAL_IF_OPENCAST__:
params_json[key] = "(optional if 'use_opencast':'true') ..."
else:
params_json[key] = "..."

return JsonResponse(data=params_json)

return HttpResponseNotAllowed(["GET"])


def get_mandatory_parameters(impl_name="") -> List[str]:
"""Return the mandatory parameters of the implementation."""
def get_mandatory_parameters(impl_name=""):
"""Return the mandatory parameters of the implementation.
Args:
impl_name (str): The name of the implementation to retrieve mandatory parameters for.
Returns:
dict[str, str]: A dict with the mandatory parameters for the specified implementation.
"""
if impl_name in __MANDATORY_PARAMETERS__:
return __MANDATORY_PARAMETERS__[impl_name]
if impl_name.lower() in __MANDATORY_PARAMETERS__:
Expand All @@ -153,7 +171,19 @@ def get_mandatory_parameters(impl_name="") -> List[str]:
return __MANDATORY_PARAMETERS__[impl_name.title()]
if impl_name.upper() in __MANDATORY_PARAMETERS__:
return __MANDATORY_PARAMETERS__[impl_name.upper()]
return [""]
return {}


def get_missing_params(config: dict, parameters: dict):
"""Compare parameters with configuration and returns missing ones."""
missing = ""
use_opencast = config.get("use_opencast", "").lower() == "true"
for parameter in parameters:
if parameter not in config.keys():
if use_opencast and parameter in __OPTIONAL_IF_OPENCAST__:
continue
missing += "'" + parameter + "':'...',"
return missing


def validate_json_implementation(broadcaster: Broadcaster) -> bool:
Expand All @@ -164,8 +194,9 @@ def validate_json_implementation(broadcaster: Broadcaster) -> bool:
"'piloting_conf' value is not set for '" + broadcaster.name + "' broadcaster."
)
return False

try:
decoded = json.loads(conf)
config = json.loads(conf)
except Exception as e:
logger.error(
"'piloting_conf' has not a valid Json format for '"
Expand All @@ -177,16 +208,14 @@ def validate_json_implementation(broadcaster: Broadcaster) -> bool:

parameters = get_mandatory_parameters(broadcaster.piloting_implementation)

if not parameters <= decoded.keys():
mandatory = ""
for value in parameters:
mandatory += "'" + value + "':'...',"
missing = get_missing_params(config, parameters)

if missing:
logger.error(
"'piloting_conf' format value for '"
"'piloting_conf' value for '"
+ broadcaster.name
+ "' broadcaster must be like: "
+ "{"
+ mandatory[:-1]
+ "' must define: {"
+ missing
+ "}"
)
return False
Expand Down Expand Up @@ -277,9 +306,9 @@ def __init__(self, broadcaster: Broadcaster):
application=conf["application"],
)

def copy_file_needed(self) -> bool:
"""Implement copy_file_needed from PilotingInterface."""
return False
def video_creation_method(self):
"""Implement video_creation_method from PilotingInterface."""
return CREATE_VIDEO_FROM_FS

def can_split(self) -> bool:
"""Implement can_split from PilotingInterface."""
Expand Down Expand Up @@ -338,7 +367,7 @@ def is_recording(self, with_file_check=False) -> bool:
else:
return True

def start_recording(self, event_id, login=None) -> bool:
def start_recording(self, event_id) -> bool:
"""Implement start_recording from PilotingInterface."""
logger.debug("Wowza - Start record")
json_conf = self.broadcaster.piloting_conf
Expand All @@ -347,8 +376,6 @@ def start_recording(self, event_id, login=None) -> bool:
self.url + "/instances/_definst_/streamrecorders/" + conf["livestream"]
)
filename = str(event_id) + "_" + self.broadcaster.slug
if login is not None:
filename = login + "_" + filename
data = {
"instanceName": "",
"fileVersionDelegateName": "",
Expand Down Expand Up @@ -493,17 +520,21 @@ class Smp(PilotingInterface):
def __init__(self, broadcaster: Broadcaster):
self.broadcaster = broadcaster
self.url = None
self.use_opencast = False
if self.check_piloting_conf():
conf = json.loads(self.broadcaster.piloting_conf)
url = "{server_url}/api/swis/resources"
self.url = url.format(
server_url=conf["server_url"],
# smp_version=conf["smp_version"],
)
self.use_opencast = conf["use_opencast"].lower() == "true"

def copy_file_needed(self) -> bool:
"""Implement copy_file_needed from PilotingInterface."""
return True
def video_creation_method(self):
"""Implement video_creation_method from PilotingInterface."""
if self.use_opencast:
return CREATE_VIDEO_OPENCAST
return CREATE_VIDEO_FROM_FTP

def can_split(self) -> bool:
"""Implement can_split from PilotingInterface."""
Expand Down Expand Up @@ -543,7 +574,7 @@ def is_recording(self, with_file_check=False) -> bool:

return self.verify_smp_response(response, "result", "recording")

def start_recording(self, event_id, login=None) -> bool:
def start_recording(self, event_id) -> bool:
"""Implement start_recording from PilotingInterface."""
logger.debug("Smp - Start record")
json_conf = self.broadcaster.piloting_conf
Expand All @@ -552,7 +583,6 @@ def start_recording(self, event_id, login=None) -> bool:

event = Event.objects.filter(id=event_id).first()
filename = event.slug if event else str(event_id) + "_" + self.broadcaster.slug
login = login if login else "unknown"
body = json.dumps(
[
{"uri": "/record/1/root_dir_fs", "value": "internal"},
Expand All @@ -562,9 +592,11 @@ def start_recording(self, event_id, login=None) -> bool:
"recording": "record",
"location": "internal",
"metadata": {
"course_id": event_id,
"creator": event.owner.username,
"title": filename,
"creator": login,
"description": "launch from Pod",
"description": "",
"relation": "",
},
},
},
Expand Down Expand Up @@ -637,6 +669,10 @@ def copy_file_to_pod_dir(self, filename):
"""Implement copy_file_to_pod_dir from PilotingInterface."""
logger.debug("Smp - Copy file to Pod dir")

# not needed if file is sent to the Recorder
if self.use_opencast:
return True

json_conf = self.broadcaster.piloting_conf
conf = json.loads(json_conf)

Expand Down
Loading

0 comments on commit d5f3f99

Please sign in to comment.