Categorie: Blog

Bent u [A] voor, [B] héél erg voor, of [C] extatisch over decentralisatie in de Kerk?

Deze week heb ik twee kerkelijke bevragingen ingevuld. Vicariaat Antwerpen wil weten welk vlees het in de kuip heeft aan de hand van de PKG (post-kritische geloofsschaal) en SPES-bevragingen. De factulteit theologie van de KULeuven lanceerde op twitter een bevraging om input te verzamelen voor de synode over synodaliteit in 2022. 

Over PKG- en SPES- bevragingen heb ik al gepbuliceerd in Geroosterd geloof en Standaardafwijkende geloofshouding en vond ik het artikel The Post-Critical Belief (PCB) Scale and the ECSIP (Leuven) Project: Garbage in … Garbage Out, dat een mooie neerslag bevat van het gevoel dat me bekroop bij het (opnieuw) invullen ervan. Ik dacht het daarmee zowat gehad te hebben…

De bevraging over de synode over synodaliteit slaat werkelijk alles op vlak van vooringenomenheid, stereotypering en knulligheid. Zowat alle vragen dragen argumenten aan waarom je vóór decentralisatie kan zijn. Als je als respondent die mening toevallig niet deelt, kan je natuurlijk overal “helemaal niet mee eens” invullen, maar de opsteller zou toch van iets meer inbeeldingsvermogen getuigd hebben, mochten er meer serieuze argumenten tegen decentralisatie ter toetsing voorgelegd worden (uitgenomen die vraag op de laatste pagina om te checken of je toevallig een “voorstander van een dictatoriaal politiek landsbestuur” zou zijn, want dat verklaart natuurlijk alles).

Mijn persoonlijke mening daarover paste duidelijk niet in het kader.

De samenleving verandert, en ze verandert snel. Het zijn geen veranderingen die de Kerk goed doen. Het voor de hand liggende antwoord is duidelijk: de Kerk moet mee veranderen! En als Rome er niet voor wil zorgen, moeten we het zelf kunnen regelen! 

Met veel mooie pneumologische pennenvruchten verpakt, zal dit de teneur zijn die uit de bevraging naar voor komt en die het debat op en over de synode zal overheersen. En eens de buit binnen is, begint het echte werk, want het verlanglijstje is lang…

De samenleving verandert echter zo snel en zo onbeheerst dat ik net daarom het tegenovergestelde antwoord zou geven: de Kerk mag niet mee veranderen! Juist nu kan de Kerk, die vooralsnog als enige instelling mag gelden die zich niet in de maalstroom laat meesleuren, zich oprichten als een baken van standvastigheid in de verwarring. Juist nu kan de Kerk de daad van het geloof en alle deugden die daaruit voortkomen, materialiseren in de trouw aan de leer en aan een gezag dat onderwijst en bewaart. 

De samenleving verandert en hoewel ons wordt voorgespiegeld dat we op weg zijn naar een nieuw soort utopia, is het juist aan de Kerk, die in de loop der eeuwen het ene utopia na het andere heeft zien komen en gaan, om te toetsen of al die veranderingen inderdaad het zaad van het goede bevatten en of de vruchten uiteindelijk niet zuur zullen zijn. 

De samenleving verandert, maar daarom is dat geen spontane evolutie. MSM, Big Tech, supra-nationale politieke instellingen, ik ga niet zeggen dat er een groot complot achter zit, maar ze hebben de middelen om de veranderingen te sturen en ze gebruiken die! De Kerk heeft zelf die middelen niet (meer), maar het minste dat ze kan doen is zich schrap zetten tegen de druk van organisaties die er vooral sterk in zijn, onder een laagje humaan vernis hun eigen belangen te dienen.

De samenleving verandert, en de rol van de staat in het dagelijkse leven wordt steeds groter, precies in die landen waar de roep om decentralisatie van het kerkbestuur het sterkst weerklinkt. In België is de kerk uiterst volgzaam in haar relatie met de staat. Overheden hebben er alle belang bij dat de Kerk “mee evolueert”, want anders zou die de burgers erop kunnen wijzen dat de overheid het middenveld steeds meer regels en beperkingen oplegt, die vrije (morele) keuzes versmachten. Omdat zij universeel is, kan de kerk interne hefbomen gebruiken om te vermijden dat lokale kerken zich onder curatele van de wereldlijke overheden laten stellen. Door te decentraliseren geeft ze die hefbomen uit handen, wat precies nu onverstandig zou zijn.

De uitnodiging tot de bevraging hoopt dat de antwoorden de synode zullen “aanmoedigen tot moedig overleg en krachtdadige beslissingen”… Nee, de antwoorden op déze bevraging zullen slechts de richting wijzen van de gemakkelijkheidsoplossing. De werkelijke moed op de synode zal erin bestaan de antwoorden die op zichzelf zo logisch en vanzelfsprekend lijken, in vraag te durven stellen. De werkelijke krachtdadige beslissing van de synode zal erin bestaan de storm te trotseren, zoals Christus dat deed, in het wankele bootje dat gegrepen was door de storm.

Raspberry Pi Broadcaster DIY

This article describes how I set up a Raspberry Pi with camera module for fully automated broadcasting live streams to youtube and facebook. 

I’m not going to productize it, but let’s give it a name: the RPB (Raspberry Pi Broadcaster).

It is not easy to reconstruct a step-by-step guide, because I went through quite some trial and error. You’ll rather find a collection of related topics.

Features

The RPB’s main control is a webpage, hosted on the Raspberry Pi and accessible on pc or mobile phone, from the internet (remote) or from the local network. It allows to preview video and sound and to start and stop livestreaming to preconfigured destinations. There’s also a function to reboot the RPB, just in case…

The controls are very minimalistic, because the RPB is to be used by low-tech users. The RPB is deployed in a church, where each week holy mass is livestreamed.

For the administrator… there’s nothing. Most of the setup is hardcoded, which is not that bad, because the amount of code is very limited.

Here’s what the interface looks like:

You can

  • enter a title and description for the stream,
  • preview the video and audio and
  • start the stream when ready to go.

Architecture

Let’s have a look at the overall architecture of the RPB.

The bold arrows show how the video flows. The input ffmpeg is always running and launched by the nginx server. The output ffmpeg is only running when a livestream is being broadcasted. It is launched by my cgi script, after a negotiation with the youtube and facebook api’s to obtain the rtmp addresses to stream to.

Software tools

I’m more of a scripter than a programmer. There’s some html, cgi, python, bash and a tiny bit of javascript. And lots of configuration files.

The tools that do most of the work are ffmpeg, nginx and fcgiwrap. The youtube live streaming api and facebook graph api also come into play.

Equipment

My equipment list:

  • Raspberry Pi 4
  • HQ camera module
  • A canon 25mm C-mount lens from ebay
  • A UPS module 
  • A USB audio converter, connected up to our amplifier
  • A home-made control box featuring a button, a switch and a led, hooked up to the Pi’s GPIO pins (not sure how I’ll use it, though)

Remote access to the RPB

Most of my development is done at home, with the RPB in it’s fixed position at church. Therefor it’s important to have remote access.

The first step is to setup port forwarding on the router of the church network. 

The second step is to setup dynamic dns, so I can access the RPB using a fixed domain name. I use dynu, because it’s free and requires no monthly reconfirmations. On the RPB I have ddclient running, configured in /etc/ddclient.conf

protocol=dyndns2
use=web, web=checkip.dyndns.org/
server=api.dynu.com
login=************
password='************'
*****.ddnsfree.com

Running sudo ddclient -daemon=0 -debug -verbose -noquiet makes sure that ddclient is working OK.

Running sudo service ddclient start to launch the service.

Most of the time, I connect via mosh, providing ssh functionality and better coping with network hick-ups. If I need a GUI-tool, there’s realvnc

Ssh and vnc are preconfigured on the pi and only need activation via sudo raspiconfig.

These are the ports that I forward to the RPB:

blank

(the screenshot is old, I now use nginx i.o. lighttpd as web server, because it can handle rtmp video streaming)

Vnc can also be done through ssh tunneling, it’s probably safer, but also more cumbersome: after running ssh -L 1111:localhost:5901 pi@*****.ddnsfree.com, the RPB vnc server can be accessed via localhost:1111.

An alternative for access through port forwarding and dynamic dns, is using the wormhole concept, but I only found a commercial supplier Dataplicity, and even at the low price of $3 per month, I can spend that better.

On the local network, finding a headless Raspberry Pi has always challenged me, fiddling with fing on my mobile phone and commands like sudo nmap -sP 192.168.1.*, until I found out that it can run a bonjour service using the linux avahi service and make itself known for other devices using a hostname, defaulting to raspberrypi.local, which is of course configurable. Or you can find it by running avahi-browse --all

Raspberry Pi UPS

If someone switches off the power (which happens more than often), a naked Raspberry Pi is getting a hard shutdown, causing potential (fatal) corruption on the SD-card. 

There are different sorts of UPS modules available, with lithium batteries, with capacitors, with regular rechargeable batteries. The choices are plenty, but it was hard to find one that meets the Raspberry Pi 4 specs, with it’s 3A power supply, which I may need to feed the camera. 

I finally settled for UPS PIco HV4.0B Stack, a modular system where you can choose several types of batteries and combine it with capacitors. I only need power to guarantee a proper shutdown on powercuts, but I want to be on the safe side.

Nginx web server with rtmp module

It took me a while to find out how powerful the nginx web server is, with it’s rtmp module. It can be used to consume or produce streams, relay them or convert them to hls for web viewing, to only name a few things.

I installed it from source, adding the rtmp module, using the instructions here. This means that all nginx files are in /usr/local/nginx. I had to create my own service file, under /etc/systemd/system/nginx.service:

[Unit]
Description=The nginx server
After=network-online.target remote-fs.target nss-lookup.target
[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/bin/sleep 10
ExecStartPre=/usr/local/nginx/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/local/nginx/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/usr/local/nginx/sbin/nginx -g 'daemon on; master_process on;' -s reload
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /usr/local/nginx/logs/nginx.pid
TimeoutStopSec=5
KillMode=mixed
[Install]
WantedBy=multi-user.target

Because I had some problems with nginx trying to run before network was active, I changed the After= line and I added the line ExecStartPre=/bin/sleep 10

Another add-on is fcgiwrap, which I installed with apt. To make live easier, I want to run my cgi scripts as user pi, which is configured in /lib/systemd/system/fcgiwrap.service by adding under [Service]:

User=pi
Group=pi

To make sure these things are started automatically:

sudo systemctl enable nginx.service

sudo systemctl enable fcgiwrap

Nginx config file

My nginx.conf is in /usr/local/nginx/conf, because I compiled the program from source. It may be elsewhere if it’s installed from a package, but make sure you also have the rtmp module!

I’ll just run through the entries that I customized:

I want to run the server as user pi, that avoids lot’s of headaches when getting into cgi scripting. I know, it’s probably not secure, but you’ll see that I don’t really worry. I won’t do any ssl (https) stuff either.

user  pi pi;

I think it’s convenient having all logging in /var/log/syslog. But even now, I still get parallell logging in /usr/local/nginx/logs/error.log

error_log  syslog:server=unix:/dev/log,tag=nginx,nohostname,severity=info;

In following section, the main architecture block is setup. 

  1. The rtmp module is listening on port 1935 for rtmp input
  2. It launches the ffmpeg process that will be providing this input. 
  3. It converts the input into hls video files
  4. It pushes (copies) the input to port 1936

Note how rtmp addresses are composed of a domain and a port and then an “application” name, followed by a “stream” name. The stream name is defined by the application that creates the stream, it is not part of the nginx configuration. In the topic on ffmpeg, I discuss the ffmpeg command that is run in the ffmpeg.sh script and you’ll see that the full rtmp address is “rtmp://localhost:1935/webcam/hhart”.

rtmp {
    server {
        listen 1935;
        chunk_size 4000;
        application webcam {
            live on;
            hls on;
            hls_path /tmp/webcam;
            deny play all;
            push rtmp://127.0.0.1:1936/webcam/;
            exec_static /usr/lib/cgi-bin/ffmpeg.sh;
            respawn on;
        }
    }
}

The following entries are all inside the http { … } block.

Also the access log goes into syslog:

access_log syslog:server=unix:/dev/log,tag=nginx,nohostname,severity=info combined;

The following entries are nested in a server { listen 80; …} block.

My user interface is a cgi webpage. It is in /usr/lib/cgi-bin. Cgi requires fcgiwrap to be installed and running! I had some trouble because the default nginx.conf referred to a fastcgi_params file that didn’t exist, but which I eventually found in /usr/local/nginx/conf.

location /cgi-bin/ {
        gzip off;
        root  /usr/lib;
        fastcgi_pass  unix:/var/run/fcgiwrap.socket;
        include /usr/local/nginx/conf/fastcgi_params;
        fastcgi_param SCRIPT_FILENAME  /usr/lib$fastcgi_script_name;
}

Because I have only one active webpage, I map it to the root address of the server.

rewrite ^/$ /cgi-bin/stream_auto.cgi;

In the rtmp section, hls files were produced, and here’s where they will be served to the outside world. 

location /webcam {
    add_header 'Cache-Control' 'no-cache';
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Expose-Headers' 'Content-Length';
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Max-Age' 2592000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
    }
    types {
        application/dash+xml mpd;
        application/vnd.apple.mpegurl m3u8;
        video/mp2t ts;
    }
    root /tmp/;
}

The user interface cgi script

The user interface for the RPB is a cgi script written in python. It’s very custom-made, so I’m going to extract some parts and explain.

The script is called stream_auto.cgi. It is in the /usr/lib/cgi-bin directory that is known in the nginx configuration and I also redirect the domain root address to this script.

The configuration of available streams is hard-coded. We typically broadcast to one youtube channel and two facebook pages simultaneously. I’ve hardcoded another set of outputs for testing, I switch between both by changing the code.

streams_real = [
        {   
            'name': "Parochie Heilig Hart Antwerpen",
            'type': "youtube",
            'view': "https://www.youtube.com/channel/UCHEqX9lP-1Ro2zHy0kIdWwA/live"
        },  
        {   
            'name': "Federatie Warm-Noord",
            'type': "facebook",
            'view': "https://www.facebook.com/Federatie-Warm-Noord-106892174602365/live/"
        },  
        {   
            'name': "Parochie Heilig Hart Antwerpen",
            'type': "facebook",
            'view': "https://www.facebook.com/antonius.heilighart.antwerpen/live/"
        }
]

To preview the hls stream, I use video.js in my webpage. Here’s the html code from my jinja template. I recall that video.js is a pain in the *** to get styled. Even resizing it seems to be utterly impossible.

<style>
body {
   text-align: center;
}
div.video {
   padding: 12px;
}
.video-js {
   margin: auto;
}
</style>
<link href="//vjs.zencdn.net/7.8.2/video-js.min.css" rel="stylesheet">
<script src="//vjs.zencdn.net/7.8.2/video.min.js"></script>

<div class="video">
   <video id="my-video" class="video-js vjs-default-skin" 
       autoplay controls preload="auto" vjs-big-play-centered 
       data-setup='{}'>
       <source src="/webcam/hhart.m3u8" type="application/x-mpegURL"/>
   </video>
</div>

There’s a single cgi webpage, but the contents is conditionally rendered using a single jinja template with the necessary {% if …%} {% endif %} clauses. One thing the script will do, is to check if the output ffmpeg process is running. If it is running, the stop button is visible, if not, the start button. 

Here’s my very basic bash script that checks of the output ffmpeg is running (knowing that it reads from port 1936 on the nginx server!)

#!/bin/bash
ps -ef | grep -E "ffmpeg.*:1936" | grep -v grep

And here’s how to run this in python:

ps = subprocess.getoutput('./pffmpeggrep.sh').split()

The pid is retrieved from ps[1], which I’ll need when processing the stop action, killing this process.

The webpage has two cgi forms. One for starting the streaming and one for stopping the streaming. The latter is simple: kill the output ffmpeg. Youtube and facebook will notice that the stream has stopped and stop the broadcasts. The former is more complex, involving initiation of the broadcasts via the youtube and facebook api’s.

This is the form for starting the streaming: 

<form action="/cgi-bin/stream_auto.cgi" method="post">
        <input type="hidden" name="action" value="start">
        <p>Titel:<br/><input type="text" name="title" value="{{ liturgical_day|e }}" size=40></p>
        <p>Beschrijving:<br/><textarea name="description" rows=4 cols=40>Live H. Mis vanuit de Heilig-Hartkerk</textarea></p>
        <p>Wachtwoord:<br/><input type="password" name="password"></p>
        <p><input type="submit" value="Start streaming"></p>
</form>

When the page is loaded after clicking this button, this is how the input parameters are retrieved in python:

# get POST parameters
form = cgi.FieldStorage()
action = form.getvalue('action')

Starting the streaming involves these steps:

  1. running create_broadcast.py to get the youtube rtmp address (see the topic about the youtube livestream api for more details)
  2. running facebook-live-video.py to get the facebook rtmp address (see the topic about the facebook livestream api for more details)
  3. launching an ffmpeg process that reads from the rtmp stream that nginx pushes to port 1936 and copies it to all of the youtube and facebook rtmp addresses.

This is how the ffmpeg command is composed and how it is launched as a daemon process:

command = "/usr/lib/cgi-bin/ffmpeg -f flv -listen 1 -i rtmp://127.0.0.1:1936/webcam/"
for stream in streams:
    command += " -c copy -f flv '" + stream['rtmp'] + "'"
os.system("daemon --stdout=daemon.info --stderr=daemon.err -- %s" % command)

One advantage of building your own streaming platform is that you can enrich it with custom features. Here I fetch from some 3rd party website the name of today’s  liturgical day, to put it in the title field as default entry:

# get liturgical day
date = datetime.datetime.today().strftime('%Y-%m-%d')
url = "https://publication.evangelizo.ws/NL/days/" + date
resp = requests.get(url).text
data = json.loads(resp)
liturgical_day = data['data']['liturgic_title']

Another feature on the website is a button to reboot the pi, in case something’s wrong with the preview.

This bash script is launched by the cgi script and will schedule a reboot within 5 seconds. All the weird redirectioning is required so the cgi script won’t be waiting for the reboot to happen.

#!/bin/bash
( sleep 5 ; sudo reboot ) &>/dev/null & disown 

Five seconds is enough for the cgi script to return a webpage confirming that the reboot is ongoing and including a tiny bit of javascript that will automatically return the user to the main webpage after 2 minutes. I’ll just print the whole reboot cgi script here, because it’s compact and still gives a good idea of how all the html/cgi/python is hanging together:

#!/usr/bin/python3
from jinja2 import Template
import subprocess, shlex
import os
import cgi, cgitb
import logging
cgitb.enable() # this will display trace information in the browser
tm = Template('''Content-type: text/html
<!DOCTYPE html>
<html>
        <head>
                <title>Heilig Hart Livestream</title>
                <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
                <style>
                body {
                    text-align: center;
                }
                </style>
                <meta name="viewport" content="width=device-width, initial-scale=1">
        </head>
        <body>
                <h1>Heilig Hart Livestream</h1>
                <p><a href="/">Ga terug naar het hoofdscherm</a></p>
                {% if reboot %}
                <script>
                   setTimeout(function(){
                      window.location.href = '/';
                   }, 120000);
                </script>
                <p>Deze pagina schakelt over 2 minuten terug naar het hoofdscherm.</p>
                <p>(na een reboot kan het nog even duren voor je weer beeld krijgt!)</p>
                {% endif %}
                {% if report %}
                <h2>Rapport</h2>
                <pre>{{ report|e }}</pre>
                {% endif %}
                {% if not reboot %}
                <form action="/cgi-bin/shutdown.cgi" method="post">
                        <input type="hidden" name="action" value="reboot">
                        <p>Wachtwoord:<br/><input type="password" name="password"></p>
                        <p><input type="submit" value="Reboot"></p>
                </form>
                {% endif %}
        </body>
</html>
''')
report = ''
reboot = False
def main():
    global report
    global reboot
    # get POST parameters
    form = cgi.FieldStorage()
    action = form.getvalue('action')
    # handle actions
    if action == 'reboot':
        if not form.getvalue('password') == "*****": 
            report += "Wrong password"
        else:
            subprocess.getoutput('./shutdown.sh')
            report += "Rebooting... "
            reboot = True
    output = tm.render(
            report=report,
            reboot=reboot
    )
    print(output)
main()

Youtube livestream api

Probably the most important feature of the RPM is the automated streaming. No need to go to studio.youtube.com in a browser (on pc, not supported on mobile!), set up the stream and then launch the rtmp stream on some other application or device. It’s all done automatically through the api. 

I script my automation in python, so I had to install the client library first: python3 -m pip install google-api-python-client. Some extra packages were needed, but the error messages while running my script were clear enough.

As a starting point, I took the sample script create_broadcast.py. I ported it to python 3 (mainly print statements). It needed an update in the insert_stream() function, because it was missing some required parameters: framerate="variable", ingestionType="rtmp", resolution="variable". I set enableAutoStart and enableAutoStop in the insert_broadcast() function. I added some code to fetch the rtmp address from this response: insert_stream_response["cdn"]["ingestionInfo"]. Finally, I replaced print() by logging.info() and I reserve the stdout for a json string containing the information that I really need: the video id and the rtmp address.

The most important prerequisite to get the script running, is authentication. It’s a two-stepped approach: you have to authenticate once to your google account, some credentials are stored in a file, which is then used for authorizing subsequent runs of the script. This is how it’s done:

  1. go to google cloud console and create (or open) a project and activate the Youtube Data API v3 
  2. still in the google cloud console, create an oauth 2.0 client ID for a desktop app, covering scope https://www.googleapis.com/auth/youtube 
  3. download its client_secrets.json, this file has to be kept with the script (the script contains a path where it will look for it), mind: this isn’t the credentials file for accessing the api, that comes later!
  4. now run create_broadcast.py --noauth_local_webserver, open the displayed url in a browser, select the user account and the youtube channel you want to authenticate with and return the code to the script.
  5. if all goes fine, the script will now create a new stream on your youtube channel (you can see it on studio.youtube.com as a scheduled live video), but it will also produce a file create_broadcast.py-oauth2.json containing credentials that will be used to authenticate the next time the script is run. To be able to use different accounts/channels (e.g. for testing), I changed the script so I can have a set of these files, one for each account/channel, which I can select using an extra command line parameter.

Facebook livestream api

For facebook, there is no python client library, and no sample code. And documentation that only becomes clear after you understand its content (you know, it’s meant to be the other way around). At some point I thought that I had to go through app review and create a business account to get the api working, but that proved wrong. That’s only needed if you want to have an app that allows external users to send data through the api to your pages. Because I have a role on the pages where I want to publish live video, and no other user accounts are involved, it works without app review.

As for the missing client library, no big deal either, because the graph api is a very clean rest api. I had more trouble with finding out how urllib works on python3 than with finding out which requests to make (hey, requests seems the better alternative for urrlib).

Authentication is two-stepped: in the first step you create long-lived access tokens for each page you want to publish on. In the second step, these tokens authorize the use of the api, without the need for a personal login.

Very first step is creating an app: https://developers.facebook.com. Note down your app id and app secret.

For authentication step 1, I have a script facebook-token.py:

input: a short-lived user access token, which you can obtain by visiting the graph explorer and making sure these permissions are selected: publish_video, pages_show_list, pages_read_engagements, pages_manage_posts. Just copy the token on the right, you don’t have to do anything in the big fields.

input (I have it hardcoded in the script): the app id and the app secret.

step 1: turn the short-lived user access token into a long-lived user access token:

# build the URL for the API endpoint
host = "https://graph.facebook.com"
version = "/v9.0"
path = "/oauth/access_token"
params = urllib.parse.urlencode({
    "grant_type": "fb_exchange_token",
    "client_id": app_id,
    "client_secret": app_secret,
    "fb_exchange_token": short_lived_user_access_token})
url = "{host}{version}{path}?{params}".format(host=host, version=version, path=path, params=
params)
# open the URL and read the response         
resp = urllib.request.urlopen(url).read()    
# convert the returned JSON string to a Python datatype 
me = json.loads(resp)
long_lived_user_access_token = me['access_token']

step 2:  obtain long-lived user access tokens for all the pages that you manage:

# build the URL for requesting the long-lived page access token
path = "/me/accounts"
params = urllib.parse.urlencode({
    "access_token": long_lived_user_access_token})
url = "{host}{version}{path}?{params}".format(host=host, version=version, path=path, params=params)
# open the URL and read the response
resp = urllib.request.urlopen(url).read()
# write the output to a json file
with open('facebook.json', 'w') as outfile:
    json.dump(json.loads(resp), outfile, indent=4)

output: as you can see, is a json file in which you’ll easily find the required access tokens.

The above steps should be done only once.

For the live streaming itself, I have another script, facebook-live-video.py:

input: the name of the page

step 1: the script uses the page name to look up the long-lived page access token in the json file

# get long-lived Facebook page access token
with open('facebook.json') as json_file:
        data = json.load(json_file)
long_lived_page_access_token = ""
for page in data["data"]:
    if page["name"] == page_name:
        long_lived_page_access_token = page["access_token"]
        break

step 2: call the api to obtain the stream info, including the rtmp address:

# build the URL for the API endpoint
host = "https://graph.facebook.com"
version = "/v9.0"
path = "/me/live_videos"
params = {
    "status": "LIVE_NOW",
    "title": broadcast_title,
    "description": broadcast_description,
    "access_token": long_lived_page_access_token}
url = "{host}{version}{path}".format(host=host, version=version, path=path)
# open the URL and read the response
resp = requests.post(url, params=params).text
# print the returned JSON string 
print(json.dumps(json.loads(resp), indent=4))

If you’re working with access tokens, find out what each token contains using the access token debugger: https://developers.facebook.com/tools/debug/accesstoken

V4l2 and ffmpeg

Typical ffmpeg commands take a long chain of command line parameters. It’s only after a while of experimenting, that I found out that there’s actually some structure in these parameters. Take e.g. this command, which I use to read the video stream from the camera and output it as an rtmp stream into nginx:

/usr/lib/cgi-bin/ffmpeg -f v4l2 -framerate 30 -video_size 1280x720 -input_format h264 -i /dev/video0 -f alsa -ac 2 -i hw:2 -codec:v copy -codec:a aac -b:a 128k -ar 44100 -strict experimental -f flv "rtmp://localhost:1935/webcam/hhart"

There are actually three blocks:

  1. the video input: -f v4l2 -framerate 30 -video_size 1280x720 -input_format h264 -i /dev/video0
  2. the audio input: -f alsa -ac 2 -i hw:2
  3. the output: -codec:v copy -codec:a aac -b:a 128k -ar 44100 -strict experimental -f flv "rtmp://localhost:1935/webcam/hhart"

Each input block ends with an -i argument, identifying the source device. For me, /dev/video0 is my HQ camera module, controlled by the v4l2 video driver, and hw:2 is an USB audio input, controlled by the alsa audio driver. You may have different devides if you have different devices or drivers installed. Run arecord -l to see your audio input devices. If the USB adapter is card 2, you have to specify "-f alsa -i hw:2".

The output format ‘flv’ stands for flash video, a container that supports h.264 video and aac audio and that can be transferred over the rtmp protocol. You can add additional output blocks, which I’ll address elsewhere.

It is interesting to notice that I don’t convert the video in the output: my video codec is ‘copy’. The camera delivers the video in h.264 format, which ffmpeg forwards unaltered to the output, only adding the audio signal. This is of course very beneficial for the cpu load!

The data that comes out of /dev/video0 is produced by the v4l2 linux video driver. It has quite some settings to tweak, but so far I only changed the bitrate, setting it like this: v4l2-ctl --set-ctrl video_bitrate=2500000 which is 2500 kbps, suitable for youtube and facebook streaming. A more user friendly way to control the camera settings is to install python3 -m pip install git+https://github.com/antmicro/pyvidctrl, an ncurses graphical interface.

I’m not too familiar with how the audio is processed, but there is the alsamixer application to adjust volume.

An alternative for using the v4l2 drivers, is the uv4l application, which is run from the command line as a normal user, contains its own drivers for the camera and also produces a /dev/video* device. uv4l comes with its own html-server-based toolset, which I had a look at, but it didn’t suit my purposes.

On the raspberry pi os you also get the raspivid application. It can be maily useful to get an instant camera reading using the command raspivid -t 0, for when you’re e.g. finetuing the v4l2 settings, or manually focusing the lens.

The ffmpeg build that is packaged with the raspberry pi os didn’t work for me, so I had to recompile it. To see how an ffmpeg binary was built, just run it. Mine says:

  built with gcc 8 (Raspbian 8.3.0-6+rpi1)
  configuration: --enable-indev=alsa --enable-nonfree --enable-version3 --enable-static --disable-debug --disable-ffplay --disable-indev=sndio --disable-outdev=sndio --cc=gcc --enable-fontconfig --enable-openssl --enable-gmp --enable-gray --enable-libfribidi --enable-libass --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libsoxr --enable-libvorbis --enable-libopus --enable-libvo-amrwbenc --enable-libvpx --enable-libwebp --enable-libxml2 --enable-libzimg

Especially the options nonfree and openssl are important to get into facebook, which requires rtmps i.o. rtmp.

blank

Willen we eigenlijk nog wel naar de H. Mis?

Net voor de Advent had ik een goed voornemen en heb ik mijn Twitter opgekuist. Het is mijn voornaamste nieuwsbron en ik merkte dat ik er veel tijd aan besteedde om eigenlijk alleen maar zaken te lezen die nodeloos ergernis opwekken. Systematisch heb ik er alle accounts uitgegooid die artikels verspreidden met overwegend negatieve teneur. Dat waren er heel wat, en gedurende een tweetal weken merkte ik dat mijn tijdsbesteding veel lager was, dat ik veel meer zinvolle artikels oppikte zonder het gevoel te hebben iets echt relevants te missen en—niet onbelangrijk—dat ik beduidend minder ergernis ondervond.

Maar wat als het nieuws zelf ergerniswekkend is? Deze week was er weer geen ontsnappen aan. 

De blamage van onze bisschoppen

De nieuwsfeiten van de laatste week deden mijn ergernis steigeren over de houding van onze kerkelijke overheid. Vandaag verschijnt in het nieuws dat de nieuwe ‘versoepeling’ van de coronamaatregelen om erediensten met max 15 personen toe te laten is “samen uitgewerkt met de grote geloofsgemeenschappen”. In een mededeling van de bischoppen heet het dat zij “hebben kennis genomen van het voorstel van de minister”. Dat de bischoppen nooit vragende partij zijn geweest voor versoepeling van de maatregelen, wisten we al. Deze versoepeling komt er immers omdat de joodse (!) gemeenschap de regering had aangeklaagd bij de Raad van State. Blijkbaar hebben de bisschoppen bij de regering ook geen eisen gesteld, enkel “kennis genomen” van een voorstel, dat als ‘versoepeling’ nog altijd strenger is dan de maatregelen die hun Franse collega’s met succes zelf (!) hebben aangevochten bij hun Raad van State. Wat is het verschil tussen een Franse en een Belgische katholiek?

Nu schuiven de bisschoppen de verantwoordelijkheid door naar hun parochies, om uit te maken “of dit veilig en praktisch haalbaar is of niet”. Ik weet alvast dat veilig zeker haalbaar is, dat hebben we aangetoond in onze kerken tussen de twee lock-downs. Van besmettingen die door tracers zijn teruggevoerd op katholieke erediensten, heb ik nog niet gehoord en uit de VS vernam ik dat een studie van “1 million public Masses where the guidelines were in place, has shown that no outbreaks of COVID-19 have been linked to church attendance.” 

En ik weet ook dat het praktisch een gekkenwerk is om met maximum 15 personen (inc. priester/acoliet/lector) een H. Mis op te dragen. Tijdens de lock-down droeg onze pastoor twee keer in de week de Mis op, die werd live uitgezonden, en telkens probeerde hij enkele andere mensen te betrekken als “medewerkers”, want dat was toegelaten tot een maximum van 10 personen. Het leverde het gekke tafereel dat de kerk (500 vierkante meter) open was voor persoonlijk gebed voor maximum 4 personen, terwijl in de winterkapel (50 vierkante meter) met meer mensen een besloten Mis werd opgedragen voor de livestream. En nu zouden we dus een publieke eucharistieviering moeten organiseren met maximum 15 personen, terwijl er op een gewone zondag gemakkelijk 60 gelovigen kwamen? Met een maximum van 30 personen was het misschien mogelijk om twee missen mekaar te laten opvolgen, dat hadden we al in petto in juni, die week wanneer de maatregelen vier keer veranderd zijn. Wat nu gedaan, 3 missen, 4 missen? De bisschoppen hebben verzaakt de versoepeling, die er tegen hun zin is gekomen, te onderhandelen tot een realistische maatregel en schuiven nu de rommel door naar hun pastoors. Niet zo fraai!

De blamage van onze katholieken

Ik heb het natuurlijk gemakkelijk met scherp te schieten op de hoogwaardigheidsbekleders, maar de vraag mag ook worden gesteld of de gelovigen zelf eigenlijk nog wel een H. Mis willen? Er is zeker een kleine groep die alle coronamaatregelen zou negeren om toch aan het misoffer te kunnen deelnemen. Ik kan daar zelfs nog ergens inkomen, maar die houding valt natuurlijk niet redelijk te verdedigen. Als je echter ziet dat België onevenredig strenge maatregelen treft voor religieuze bijeenkomsten, houdt het argument van de veiligheid mijns inziens geen steek meer, dan is het gewoon de politiek die de factor ‘religie’ in onze samenleving even netjes op zijn plaats zet.

kaart van Europa met Belgie rood ingekleurd
België als enige land waar de H. Mis verboden is

En dan stel ik me de vraag: hoe zien de gelovigen dat? De mening van de gewone gelovige vind je natuurlijk niet in de kranten, dus ik ben weer terug waar ik begonnen ben: Twitter. Daar zitten ongetwijfeld heel wat gewone gelovigen die over een en ander hun mening laten horen. Ik vond heel wat tweets van mensen die het onrechtvaardig vinden dat gelovigen nu plots wel met 15 man bijeen mogen komen en zij niet, maar tweets van katholieken die reageren op de versoepeling, negatief of positief, kan ik op 1 hand tellen.

Toch zijn ze er, die gelovigen die terug naar de mis willen. De open brief aan de eerste minister Voor de Mis kreeg reeds 13000 handtekeningen. Ik zag op youtube een gesprek tussen de woordvoerder van de bisschoppen en de initiatiefnemer van de petitie. Over de bisschoppen hebben we het al gehad, maar Benoît de Baenst is als pastoor misschien geen ‘gewone’ gelovige, maar staat toch wat dichter bij ons en—eerlijk gezegd—ondanks de massale steun die zijn campagne vindt bij gelovigen, stelt zijn makke betoog weinig verwachtingen aan de vertegenwoordiger van de bisschoppen, hoewel die toch niet helemaal aan hetzelfde zeel trekken.

Er worden nochtans acties gevoerd. Via Twitter zag ik twee affiches van gebedsacties waarbij gelovigen op straat komen om te bidden voor hervatting van de eucharistie. Of die acties enig succes of weerklank vinden, durf ik niet zeggen. Misschien moet ik zelf maar eens gaan kijken…

blank
blank

Alles bij mekaar blijf ik dus achter met het ongemakkelijke gevoel dat de bisschoppen perfect beantwoorden aan de verwachtingen van hun gelovigen en dat hun dus eigenlijk niet veel kwalijk kan genomen worden.

En de moslims?

Ik stel me tot slot nog een andere vraag. Hoe zit het met de moslims? We weten nu al dat de joden de onredelijke beperking van de godsdienstvrijheid durven aanvechten en dat de katholieken dat helemaal niet nodig vinden. Maar wat met de islam? Voelen zij zich niet bezwaard, of staan zij misschien zowiezo losser tegenover het wekelijkse vrijdaggebed en hebben ze misschien genoeg aan hun dagelijkse gebedstijden? Zij hebben geen beschikking over (te) grote kerken, zoals de katholieken en ik vraag me af of het daarom is dat de regering obstinaat vasthoudt aan de onproportionele beperking, zodat er geen praktisch onderscheid ontstaat tussen katholieken en moslims in hun recht op uitoefening van hun godsdienst. Dat staat eigenlijk met zoveel woorden in de verklaring van de minister: “Als er een uitzondering komt, moeten we zien dat we de godsdiensten op gelijke voet behandelen”. Zo neemt solidariteit andermaal de vorm aan van gijzeling.

blank

Het betere TikTok: Bible Stories with Liv

U hebt wellicht al gehoord van TikTok? Miljoenen jongeren maken er dansjes op steeds dezelfde nummers. Leuk om naar te kijken, potentieel verslavend, maar blijkbaar voor sommigen ook een medium voor evangelisatie.

Zelf volg ik het niet (wie zou dat nu durven toegeven 😉 ), maar mijn dochter stuurt me regelmatig links van een tiktokster die filmpjes maakt over bijbelverhalen: Bible Stories with Liv. Zoals blijkbaar op het medium gangbaar, is zij de enige acteur. Als ze een vrouw speelt draagt ze een geplooide handdoek op het hoofd en als man heeft ze vaak een pet op. Dat zijn zowat de enige conventies waarvan je op de hoogte moet zijn, verder wijst het zichzelf uit.

Haar meest recente tiktok vertelt het verhaal van de annunciatie: 

@liv.breadstick

send this to someone who you think should know the Christmas story 🎄#biblestorieswithliv Luke 1:28-38,46-56; Matt 1:18-25

♬ original sound – Liv Pearsall

Haar tiktokaccount vind je hier:

https://www.tiktok.com/@liv.breadstick

Durf je niet op tiktok, geen nood, ze heeft ook een compilatie op Youtube:

blank

Antwerps videopastoraal

In de coronacrisis is de Antwerpse kathedraal heftig in de weer gegaan met videopastoraal, door niet alleen de eucharistievieringen uit te zenden, maar daarnaast ook verschillende andere programma’s te maken. Vier daarvan zijn intussen opgenomen op Alledaags Geloven: Sterke tijden, Licht op mijn pad, De schatkamer en Dagelijkse verbinding (de reeks over Handelingen met mgr. Bonny). 

Ook elders in Antwerpen heeft men het videopastoraal omarmd. De priesterbroederschap Pius-X produceert vanuit de Hemelstraat eveneens verschillende reeksen programma’s met liturgie, gebed, homilie en catechese. Op Alledaags Geloven vind je vanaf vandaag een kaart met de “Preken in het Nederlands”.

blank
Alledaags Geloven
Alledaags Geloven
Developer: Geloven Leren
Price: Free