Add initial version of live streaming

This commit is contained in:
Joel Addison 2024-04-16 09:10:03 +10:00
parent 84c3a48626
commit ae8e8f7d93
14 changed files with 317 additions and 18 deletions

View file

@ -14,29 +14,34 @@ if ! docker info >/dev/null 2>&1; then
exit 1
fi
SQLITEDB=$(pwd)/docker/symposion.sqlite
docker image build -f docker/Dockerfile -t ${IMAGE_NAME} --target symposion_dev .
docker container stop symposion
docker container rm symposion
docker container create --env-file docker/laptop-mode-env -p 28000:8000 -v $(pwd):/app/symposion_app --name symposion ${IMAGE_NAME}
docker container create --env-file docker/laptop-mode-env -p 28000:8000 -v $(pwd):/app/symposion_app -v $SQLITEDB:/tmp/symposion.sqlite --name symposion ${IMAGE_NAME}
docker container start symposion
## When we started the container and mounted . into /app/symposion_app, it hides the static/build directory
## As a kludge, re-run collectstatic to recreate it
## Possible alternative here: don't mount all of ., just mount the bits that we'd live to have update live
docker exec symposion ./manage.py collectstatic --noinput -v 0
docker exec symposion ./manage.py migrate
docker exec symposion ./manage.py loaddata ./fixtures/{conference,sites,sitetree,flatpages}.json
docker exec symposion ./manage.py create_review_permissions
docker exec symposion ./manage.py loaddata ./fixtures/sessions/*.json
docker exec symposion ./manage.py populate_inventory
if [ -e ./symposion-tools ]; then
pushd ./symposion-tools
./fixture_to_docker.sh fixtures/dev_dummy_superuser.json
./fixture_to_docker.sh fixtures/????_*.json
popd
else
echo Now creating a Django superuser. Please enter a
docker exec -it symposion ./manage.py createsuperuser --username admin1 --email root@example.com
if [ ! -e $SQLITEDB ]; then
## When we started the container and mounted . into /app/symposion_app, it hides the static/build directory
## As a kludge, re-run collectstatic to recreate it
## Possible alternative here: don't mount all of ., just mount the bits that we'd live to have update live
docker exec symposion ./manage.py collectstatic --noinput -v 0
docker exec symposion ./manage.py migrate
docker exec symposion ./manage.py loaddata ./fixtures/{conference,sites,sitetree,flatpages}.json
docker exec symposion ./manage.py create_review_permissions
docker exec symposion ./manage.py loaddata ./fixtures/sessions/*.json
docker exec symposion ./manage.py populate_inventory
if [ -e ./symposion-tools ]; then
pushd ./symposion-tools
./fixture_to_docker.sh fixtures/dev_dummy_superuser.json
./fixture_to_docker.sh fixtures/????_*.json
popd
else
echo Now creating a Django superuser. Please enter a
docker exec -it symposion ./manage.py createsuperuser --username admin1 --email root@example.com
fi
fi
set +x

View file

@ -259,6 +259,7 @@ INSTALLED_APPS = [
"pinaxcon.proposals",
"pinaxcon.registrasion",
"pinaxcon.raffle",
"pinaxcon.streaming",
"jquery",
"djangoformsetjs",
@ -580,3 +581,8 @@ VENUELESS_URL = os.environ.get('VENUELESS_URL', None)
VENUELESS_AUDIENCE = os.environ.get('VENUELESS_AUDIENCE', "venueless")
VENUELESS_TOKEN_ISSUER = os.environ.get('VENUELESS_TOKEN_ISSUER', "any")
VENUELESS_SECRET = os.environ.get('VENUELESS_SECRET', SECRET_KEY)
# Mux integration
MUX_PRIVATE_KEY_BASE64 = os.environ.get('MUX_PRIVATE_KEY_BASE64', None)
MUX_SIGNING_KEY_ID = os.environ.get('MUX_SIGNING_KEY_ID', None)
STREAMING_UI_VERSION = os.environ.get('STREAMING_UI_VERSION', 'test')

View file

@ -0,0 +1 @@
default_app_config = 'pinaxcon.streaming.apps.StreamingConfig'

View file

@ -0,0 +1,11 @@
from django.contrib import admin
from pinaxcon.streaming import models
class RoomStreamAdmin(admin.ModelAdmin):
list_display = ('room', 'day', 'stream_id', 'chat_url', 'published',)
list_filter = ('room','day','published',)
admin.site.register(models.RoomStream, RoomStreamAdmin)

View file

@ -0,0 +1,15 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class StreamingConfig(AppConfig):
name = "pinaxcon.streaming"
label = "pinaxcon_streaming"
verbose_name = _("Pinaxcon Streaming")
admin_group_name = "Streaming Admins"
def get_admin_group(self):
from django.contrib.auth.models import Group
group, created = Group.objects.get_or_create(name=self.admin_group_name)
return group

View file

@ -0,0 +1,33 @@
# Generated by Django 2.2.28 on 2024-04-15 21:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('symposion_schedule', '0008_auto_20190122_0815'),
]
operations = [
migrations.CreateModel(
name='RoomStream',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stream_id', models.CharField(max_length=255, null=True, verbose_name='Stream Identifier')),
('playback_id', models.CharField(max_length=255, null=True, verbose_name='Playback Identifier')),
('chat_url', models.CharField(max_length=255, null=True, verbose_name='Chat URL')),
('published', models.BooleanField(default=True, verbose_name='Published')),
('day', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Day')),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Room')),
],
options={
'verbose_name': 'Room Stream',
'verbose_name_plural': 'Room Streams',
'unique_together': {('room', 'day')},
},
),
]

View file

@ -0,0 +1,24 @@
import datetime
from django.db import models
from django.utils.translation import ugettext_lazy as _
from symposion.schedule.models import Day, Room
class RoomStream(models.Model):
"""Video/Chat stream information for a room at the conference."""
room = models.ForeignKey(Room, on_delete=models.CASCADE)
day = models.ForeignKey(Day, on_delete=models.CASCADE)
stream_id = models.CharField(max_length=255, verbose_name=_("Stream Identifier"), null=True)
playback_id = models.CharField(max_length=255, verbose_name=_("Playback Identifier"), null=True)
chat_url= models.CharField(max_length=255, verbose_name=_("Chat URL"), null=True)
published = models.BooleanField(default=True, verbose_name=_("Published"))
def __str__(self):
return "%s - %s" % (self.room.name, self.day.date.strftime("%a"))
class Meta:
unique_together = [('room', 'day')]
verbose_name = _("Room Stream")
verbose_name_plural = _("Room Streams")

View file

@ -0,0 +1,8 @@
from django.conf.urls import url
from pinaxcon.streaming import views
urlpatterns = [
url(r"^$", views.streaming_view, name="streaming-home"),
url(r'^feeds$', views.streaming_feeds, name="streaming-feeds"),
]

104
pinaxcon/streaming/views.py Normal file
View file

@ -0,0 +1,104 @@
import base64
import datetime
import json
import jwt
from django.http import Http404, HttpResponse
from django.shortcuts import render, get_object_or_404, redirect
from django.conf import settings
from django.contrib.auth.decorators import login_required
from registrasion.controllers.item import ItemController
from regidesk.models import CheckIn
from pinaxcon.streaming.models import RoomStream
@login_required
def streaming_view(request):
ctx = {}
return render(request, 'streaming/overview.html', ctx)
@login_required
def streaming_feeds(request):
"""Details of the currently available live streams."""
stream_data = []
user_data = {}
# Find the checkin for the current user.
checkin = _current_checkin(request)
if checkin:
user_data["code"] = checkin.code
# Find all streams for the current day
today = datetime.datetime.now().date()
streams = RoomStream.objects.filter(
day__date = today,
published = True,
)
for stream in streams:
stream_details = {
"room_id": stream.room.id,
"room_name": stream.room.name,
#"track_name": stream.room.track.name,
"stream_name": str(stream),
"playback_id": stream.playback_id,
"video_token": _mux_token(checkin, stream.playback_id, 'v'),
"thumbnail_token": _mux_token(checkin, stream.playback_id, 't'),
"chat_url": stream.chat_url,
}
stream_data.append(stream_details)
live_streams_data = {
"ui_version": settings.STREAMING_UI_VERSION,
"streams": stream_data,
"user": user_data,
}
return HttpResponse(
json.dumps(live_streams_data, indent=2),
content_type="application/json"
)
def _current_checkin(request):
# Check that they have a ticket first (only item from category 1)
TICKET_CATEGORY = 1
items = ItemController(request.user).items_purchased(
category=TICKET_CATEGORY
)
if not items:
return None
# Get token from checkin
checkin = CheckIn.objects.get_or_create(user=request.user)[0]
return checkin
def _mux_token(checkin, playback_id, audience):
"""Returns the Mux JSON Web Token (JWT) for a given video (playback_id)
that is linked to this checkin."""
if not checkin.code or not settings.MUX_PRIVATE_KEY_BASE64 or not settings.MUX_SIGNING_KEY_ID:
return None
# Based on example at https://docs.mux.com/guides/secure-video-playback
private_key = base64.b64decode(settings.MUX_PRIVATE_KEY_BASE64)
expiry_date = settings.CONF_END + datetime.timedelta(days=1)
token = {
'sub': playback_id,
'exp': expiry_date.timestamp(),
'aud': audience,
'custom': {
'checkin': checkin.code,
}
}
headers = {
'kid': settings.MUX_SIGNING_KEY_ID
}
json_web_token = jwt.encode(token, private_key, algorithm="RS256", headers=headers)
return json_web_token

View file

@ -0,0 +1,28 @@
{% extends "site_base.html" %}
{% load registrasion_tags %}
{% load lca2018_tags %}
{% load static %}
{% block head_title %}Live Streaming{% endblock %}
{% block page_title %}Live Streaming{% endblock %}
{% block content %}
<p>Live Streaming is available to everyone attending the conference.</p>
<p>We encourage you to join in the chat via the <a href="https://matrix.to/#/#everything-open:matrix.org" target="_blank">Everything Open Matrix Space</a></p>
<div id="holding-frame">
<p>There are currently no live streams available. Please check back soon.</p>
</div>
<div id="streaming-frame">
<mux-player
playback-id="EcHgOK9coz5K4rjSwOkoE7Y7O01201YMIC200RI6lNxnhs"
metadata-video-title="Test VOD"
metadata-viewer-user-id="user-id-007"
></mux-player>
</div>
{% endblock %}
{% block extra_script %}
<script src="https://cdn.jsdelivr.net/npm/@mux/mux-player"></script>
<script src="{% static 'js/streaming.js' %}" type="text/javascript"></script>
{% endblock %}

View file

@ -57,6 +57,18 @@
{% endif %}
{% endflag %}
{% flag "streaming_dashboard" %}
<div class="row">
<div class="col-12 mb-3">
<h3>Join the Conference</h3>
<p>The conference stream is now open. Please join us to watch talks.</p>
<div>
<a class="btn btn-lg btn-primary" role="button" href="/streaming">Launch Conference</a>
</div>
</div>
</div>
{% endflag %}
<div class="row">
<div class="col-md-6 mb-3 mb-md-0">
<h3>Attendee Profile</h3>

View file

@ -23,6 +23,7 @@ urlpatterns = [
path("teams/", include("symposion.teams.urls")),
path('raffle/', include("pinaxcon.raffle.urls")),
path('streaming/', include("pinaxcon.streaming.urls")),
# Required by registrasion
path('tickets/payments/', include('registripe.urls')),

View file

@ -0,0 +1,51 @@
$(function() {
const SLOT_REFRESH_INTERVAL_MS = 10 * 60 * 1000;
var lastVersion = '';
function update_streams(data) {
if (lastVersion && data.ui_version != lastVersion) {
window.location.reload();
return;
}
lastVersion = data.ui_version;
// If no streams, display holding message.
if (data.streams.length == 0) {
$('#holding-frame').show();
$('#streaming-frame').hide();
return;
}
let streamingFrame = $('#streaming-frame');
$('#holding-frame').hide();
streamingFrame.show();
var stream = data.streams[0];
const muxPlayer = document.createElement("mux-player");
muxPlayer.setAttribute("playback-id", stream.playback_id);
muxPlayer.setAttribute("metadata-video-title", stream.stream_name);
muxPlayer.setAttribute("metadata-viewer-user-id", data.user.code);
muxPlayer.tokens = {
playback: stream.video_token,
thumbnail: stream.thumbnail_token,
};
streamingFrame.empty();
streamingFrame.append(muxPlayer);
}
function load_streams() {
$.ajax({
dataType: "json",
method: "GET",
url: "/streaming/feeds",
success: update_streams
});
}
load_streams();
setInterval(load_streams, SLOT_REFRESH_INTERVAL_MS);
});