Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • aura/engine
  • hermannschwaerzler/engine
  • sumpfralle/aura-engine
3 results
Show changes
Showing
with 1432 additions and 0 deletions
-include build/base.Makefile
-include build/docker.Makefile
help::
@echo "$(APP_NAME) targets:"
@echo " init.app - init application environment"
@echo " init.dev - init development environment"
@echo " api - build models for API requests/responses"
@echo " lint - verify code style"
@echo " spell - check spelling of text"
@echo " format - apply automatic formatting"
@echo " test - run test suite"
@echo " coverage - test the code coverage"
@echo " log - tail log file"
@echo " run - start app"
@echo " release - tag and push release with current version"
$(call docker_help)
# Settings
TIMEZONE := "Europe/Vienna"
AURA_ENGINE_CORE_SOCKET := "aura_engine_socket"
AURA_ENGINE_CONFIG := ${CURDIR}/config/engine.docker.yaml
AURA_AUDIO_STORE_SOURCE := ${CURDIR}/../engine-core/audio/source
AURA_AUDIO_STORE_PLAYLIST := ${CURDIR}/../engine-core/audio/playlist
AURA_LOGS := ${CURDIR}/logs
AURA_UID := 872
AURA_GID := 872
DOCKER_RUN = @docker run \
--name $(APP_NAME) \
--network="host" \
--mount type=tmpfs,destination=/tmp \
--env-file docker.env \
-v aura_engine_socket:"/srv/socket" \
-v "$(AURA_ENGINE_CONFIG)":"/etc/aura/engine.yaml":ro \
-v "$(AURA_AUDIO_STORE_SOURCE)":"/var/audio/source":ro \
-v "$(AURA_AUDIO_STORE_PLAYLIST)":"/var/audio/playlist":ro \
-v "$(AURA_LOGS)":"/srv/logs" \
-u $(AURA_UID):$(AURA_GID) \
$(DOCKER_ENTRY_POINT) \
autoradio/$(APP_NAME)
# Targets
init.app:: pyproject.toml
poetry install
cp -n config/sample.engine.yaml config/engine.yaml
mkdir -p .cache
init.dev:: pyproject.toml
poetry install --with dev
poetry run pre-commit autoupdate
poetry run pre-commit install
cp -n config/sample.engine.yaml config/engine.yaml
mkdir -p .cache
api::
rm -rf .build
rm -rf src/aura_tank_api/*
poetry run openapi-python-client generate --path schemas/openapi-tank.json --config .openapi-client-tank.yml
cp -r .build/aura_tank_api/models src/aura_tank_api
cp .build/aura_tank_api/py.typed src/aura_tank_api
cp .build/aura_tank_api/types.py src/aura_tank_api
rm -rf .build
rm -rf src/aura_steering_api/*
poetry run openapi-python-client generate --path schemas/openapi-steering.json --config .openapi-client-steering.yml
cp -r .build/aura_steering_api/models src/aura_steering_api
cp .build/aura_steering_api/py.typed src/aura_steering_api
cp .build/aura_steering_api/types.py src/aura_steering_api
lint::
poetry run python3 -m flake8 .
spell::
poetry run codespell $(wildcard *.md) docs src tests config contrib
format::
poetry run python3 -m isort .
poetry run black .
test::
poetry run python3 -m unittest discover . --pattern "test_*.py"
coverage::
poetry run coverage run -m unittest discover . --pattern "test_*.py" && poetry run coverage report -m && poetry run coverage xml
log::
tail -f logs/engine.log
run::
poetry run python3 -m aura_engine.app
release:: VERSION := $(shell poetry version)
release:: VERSION := $(lastword $(subst |, ,$(VERSION)))
release:: VERSION := ${shell echo $(VERSION) | sed -r "s/\\x1B[\\x5d\[]([0-9]{1,3}(;[0-9]{1,3})?(;[0-9]{1,3})?)?[mGK]?//g"}
release::
@echo "Releasing '${VERSION}'..."
git tag $(VERSION)
git push origin $(VERSION)
@echo "Release '$(VERSION)' tagged and pushed successfully."
\ No newline at end of file
# Aura Engine
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://docs.aura.radio/en/latest/contribute/code_of_conduct.html) [![Latest Release](https://gitlab.servus.at/aura/engine/-/badges/release.svg)](https://gitlab.servus.at/aura/engine/-/releases) [![pipeline status](https://gitlab.servus.at/aura/engine/badges/main/pipeline.svg)](https://gitlab.servus.at/aura/engine/-/commits/main) [![coverage report](https://gitlab.servus.at/aura/engine/badges/main/coverage.svg?job=run_test_cases)](https://gitlab.servus.at/aura/engine/-/commits/main) [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://gitlab.servus.at/aura/engine/-/commits/main) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
<img src="https://gitlab.servus.at/aura/aura/-/raw/main/assets/images/aura-engine.png" width="120" align="right" />
AURA Engine is a scheduling service to control the playout of _Engine Core_.
This documentation is primarily meant for developers. For using the AURA Community Radio Suite
check out the documentation at [docs.aura.radio](https://docs.aura.radio/)
To learn more about Automated Radio go to [aura.radio](https://aura.radio).
## Overview
The following diagram gives an simplified overview on the architecture.
```mermaid
flowchart LR
api-- GET calendar -->steering[Steering API]
api-- GET playlist -->tank[Tank API]
ea[Engine API]
events-- on_boot\non_sick\non_resurrect -->monitor[Monitor]
events-- on_play\non_fallback_active -->clock[Clock]
clock-- POST clock_data -->ea
monitor-- PING heartbeat -->monserver[Monitoring Server]
monitor-- POST health_data -->ea
mixer-- UNIX SOCKET -->engine_core
subgraph external
ea
monserver
engine_core
steering
tank
end
subgraph engine
engine_instance[Engine]-.-oplayer & events & scheduler
scheduler-- control\ncommand\ntimer -->player[Player]
player-- control action -->mixer
events[Event Dispatcher *]
subgraph scheduling
api[Fetch & Cache API]
timetable<-->api
scheduler<-->timetable[Timetable Management]
scheduler-- cycle -->scheduler
end
subgraph core[core]
mixer[Mixer]
end
subgraph plugins
clock
monitor
end
end
style external fill:#FFF
style ea fill:#FFF,stroke:#333,stroke-width:1px
style steering fill:#FFF,stroke:#333,stroke-width:1px
style tank fill:#FFF,stroke:#333,stroke-width:1px
style monserver fill:#FFF,stroke:#333,stroke-width:1px
style engine_core fill:#FFF,stroke:#333,stroke-width:1px
```
(\*) **Event Dispatcher**: Events are issued and consumed in multiple locations of Engine. The diagram shows only a few ones, in order to support the overview. Similar to other artifacts, which are not part of this diagram.
## Prerequisites
Before you begin, ensure you have met the following requirements:
- Operating system: Debian 12, Ubuntu 23.10 or newer
- `git`, `make`
- [Docker](https://www.docker.com/), optional if you want to run in a container
- [Python 3.11+](https://www.python.org/downloads/)
- [Poetry](https://python-poetry.org/)
Ensure that you have also dependencies such as `steering`, `tank`, `dashboard`, `engine-core`, and `engine-api` up and running.
## Preparation
### Initialize environment
Install dependencies and prepare config file:
```shell
make init.app
```
This also creates a default configuration file at `config/engine.yaml`.
For development install with:
```shell
make init.dev
```
Note, if some configuration exists under `/etc/aura/engine.yaml` the configuration by default is drawn from there. This overrides any configuration located in the local configuration file.
## Configuration
Edit the configuration file `config/engine.yaml`. Verify or change at least these config options:
```yaml
# The secret which is used to authenticate against Tank
api_tank_secret: aura-engine-secret
```
## Running Engine
To start the Engine execute:
```shell
make run
```
## Docker
For production deployments follow the Docker Compose installation instruction for _AURA Playout_ at [docs.aura.radio](https://docs.aura.radio/).
The following instructions are meant for development.
### Build with Docker
Build your own, local Docker image
```shell
make docker.build
```
### Run with Docker
Run the locally build image
```shell
make docker.run
```
### Release to DockerHub
Releasing a new version to DockerHub
```shell
make docker.push
```
Usually this is not required, as it is done automatically by the CI/CD pipeline.
## Read more
- [Engine Developer Guide](docs/developer-guide.md)
- [Setting up the Audio Store](https://docs.aura.radio/en/latest/administration/setup-audio-store.html)
- [docs.aura.radio](https://docs.aura.radio)
- [aura.radio](https://aura.radio)
# Base config for AURA Makefiles
# Include this at the top of other Makesfiles
.DEFAULT_GOAL := help
APP_NAME := $(shell basename $(dir $(abspath $(dir $$PWD/Makefile))))
UID = $(shell id -u)
GID = $(shell id -g)
\ No newline at end of file
# Docker targets for AURA Makefiles
# Help
define docker_help
@echo " docker.build - build docker image"
@echo " docker.push - push docker image"
@echo " docker.run - start app in container"
@echo " docker.run.i - start app in container (interactive mode)"
@echo " docker.run.bash - start bash in container"
@echo " docker.run.debug - start bash and mount . into container"
@echo " docker.restart - restart container"
@echo " docker.stop - stop container"
@echo " docker.rm - stop and remove container"
@echo " docker.log - container logs for app"
@echo " docker.bash - enter bash in running container"
endef
# Dependencies
docker.deps:
@which docker
# Targets
docker.build:: docker.deps
@docker build -t autoradio/$(APP_NAME) .
docker.push:: docker.deps
@docker push autoradio/$(APP_NAME)
docker.run:: DOCKER_ENTRY_POINT := -d
docker.run:: docker.deps
$(DOCKER_RUN)
docker.run.i:: DOCKER_ENTRY_POINT := -it --rm
docker.run.i:: docker.deps
$(DOCKER_RUN)
docker.run.bash:: DOCKER_ENTRY_POINT := --entrypoint bash -it --rm
docker.run.bash:: docker.deps
$(DOCKER_RUN)
docker.run.debug:: DOCKER_ENTRY_POINT := -v "$(CURDIR)":"/srv" --entrypoint bash -it --rm
docker.run.debug:: docker.deps
$(DOCKER_RUN)
docker.restart:: docker.deps
@docker restart $(APP_NAME)
docker.stop:: docker.deps
@docker stop $(APP_NAME)
docker.rm:: docker.stop
@docker rm $(APP_NAME)
docker.log:: docker.deps
@docker logs $(APP_NAME) -f
docker.bash:: docker.deps
@docker exec -it $(APP_NAME) bash
##############################################
# Engine Configuration #
##############################################
general:
# Path to the engine-core socket directory relative to the engine project root
socket_dir: ../engine-core/socket
# Directory to store temporary data
cache_dir: ./.cache
log:
# Directory where the log file resides
directory: logs
# Possible values: debug, info, warning, error, critical
level: debug
monitoring:
heartbeat:
# Seconds how often the vitality of Engine Core should be checked (default: 1)
frequency: 1
# Host where heartbeat is sent to (disabled if empty string)
host: ""
# Some UDP port
port: 43334
api:
steering:
# The URL to get the health status
status: http://localhost:8000/api/v1/
# The URL to get the Calendar via Steering
calendar: http://localhost:8000/api/v1/program/playout
tank:
# The session name which is used to authenticate against Tank
session: engine
# The secret which is used to authenticate against Tank
secret: rather-secret
# The URL to get the health status
status: http://localhost:8040/healthz
# The URL to get playlist details via Tank
playlist: http://localhost:8040/api/v1/playlists/${ID}
engine:
# Engine ID (1 or 2)
number: 1
# Engine API availability check
status: http://localhost:8008/api/v1/ui/
# Engine API endpoint to store playlogs
store_playlog: http://localhost:8008/api/v1/playlog
# Engine API endpoint to store clock information
store_clock: http://localhost:8008/api/v1/clock
# Engine API endpoint to store health information
store_health: http://localhost:8008/api/v1/source/health/${ENGINE_NUMBER}
scheduler:
# Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs)
# Either provide an absolute base path or a relative one starting in the `engine` directory. In case of `engine-core` running in docker use `/var/audio/source`
audio:
source_folder: ../engine-core/audio/source
source_extension: .flac
# Folder holding M3U Playlists to be scheduled in form of Engine Playlists (similar as audio source folder above)
playlist_folder: ../engine-core/audio/playlist
# Offset in seconds how long it takes for Liquidsoap to actually execute a scheduler command; Crucial to keep things in sync
engine_latency_offset: 0.5
# How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show is aired
fetching_frequency: 30
# The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
scheduling_window_start: 60
scheduling_window_end: 60
# How many seconds before the actual schedule time the entry should be pre-loaded. Note to provide enough timeout for
# contents which take longer to load (big files, bad connectivity to streams etc.). If the planned start time is in
# the past the offset is ignored and the entry is played as soon as possible
preload_offset: 15
# Sometimes it might take longer to get a stream connected. Here you can define a viable length.
# But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
# overall playout, its delays and possible fallbacks
input_stream:
buffer: 3.0
# Fade duration when selecting another mixer input (seconds)
fade_in_time: 1.5
fade_out_time: 1.5
##############################################
# Engine Configuration #
##############################################
general:
# Path to the engine-core socket directory relative to the engine project root
socket_dir: /srv/socket
# Directory to store temporary data
cache_dir: /tmp
log:
# Directory where the log file resides
directory: logs
# Possible values: debug, info, warning, error, critical
level: ${AURA_ENGINE_LOG_LEVEL}
monitoring:
heartbeat:
# Seconds how often the vitality of Engine Core should be checked (default: 1)
frequency: ${AURA_ENGINE_HEARTBEAT_FREQUENCY}
# Host where heartbeat is sent to (disabled if empty string)
host: ${AURA_ENGINE_HEARTBEAT_SERVER}
# Some UDP port
port: ${AURA_ENGINE_HEARTBEAT_SERVER_PORT}
api:
steering:
# The URL to get the health status
status: ${AURA_STEERING_BASE_URL}api/v1/
# The URL to get the Calendar via Steering
calendar: ${AURA_STEERING_BASE_URL}api/v1/program/playout
tank:
# The session name which is used to authenticate against Tank
session: ${AURA_TANK_ENGINE_USER}
# The secret which is used to authenticate against Tank
secret: ${AURA_TANK_ENGINE_PASSWORD}
# The URL to get the health status
status: ${AURA_TANK_BASE_URL}healthz
# The URL to get playlist details via Tank
playlist: ${AURA_TANK_BASE_URL}api/v1/playlists/${ID}
engine:
# Engine ID (1 or 2)
number: 1
# Engine API availability check
status: ${AURA_ENGINE_API_BASE_URL}api/v1/ui/
# Engine API endpoint to store playlogs
store_playlog: ${AURA_ENGINE_API_BASE_URL}api/v1/playlog
# Engine API endpoint to store clock information
store_clock: ${AURA_ENGINE_API_BASE_URL}api/v1/clock
# Engine API endpoint to store health information
store_health: ${AURA_ENGINE_API_BASE_URL}api/v1/source/health/${ENGINE_NUMBER}
scheduler:
# Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs)
# Either provide an absolute base path or a relative one starting in the `engine` directory. In case of `engine-core` running in docker use `/var/audio/source`
audio:
source_folder: /var/audio/source
source_extension: .flac
# Folder holding M3U Playlists to be scheduled in form of Engine Playlists (similar as audio source folder above)
playlist_folder: /var/audio/playlist
# Offset in seconds how long it takes for Liquidsoap to actually execute a scheduler command; Crucial to keep things in sync
engine_latency_offset: ${AURA_ENGINE_LATENCY_OFFSET}
# How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show is aired
fetching_frequency: 30
# The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
scheduling_window_start: 60
scheduling_window_end: 60
# How many seconds before the actual schedule time the entry should be pre-loaded. Note to provide enough timeout for
# contents which take longer to load (big files, bad connectivity to streams etc.). If the planned start time is in
# the past the offset is ignored and the entry is played as soon as possible
preload_offset: 15
# Sometimes it might take longer to get a stream connected. Here you can define a viable length.
# But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
# overall playout, its delays and possible fallbacks
input_stream:
buffer: 3.0
# Fade duration when selecting another mixer input (seconds)
fade_in_time: ${AURA_ENGINE_FADE_IN_TIME}
fade_out_time: ${AURA_ENGINE_FADE_OUT_TIME}
##############################################
# Engine Configuration #
##############################################
general:
# Path to the engine-core socket directory relative to the engine project root
socket_dir: ../engine-core/socket
# Directory to store temporary data
cache_dir: ./.cache
log:
# Directory where the log file resides
directory: logs
# Possible values: debug, info, warning, error, critical
level: info
monitoring:
heartbeat:
# Seconds how often the vitality of Engine Core should be checked (default: 1)
frequency: 1
# Host where heartbeat is sent to (disabled if empty string)
host: ""
# Some UDP port
port: 43334
api:
steering:
# The URL to get the health status
status: http://localhost:8000/api/v1/
# The URL to get the Calendar via Steering
calendar: http://localhost:8000/api/v1/program/playout
tank:
# The session name which is used to authenticate against Tank
session: engine
# The secret which is used to authenticate against Tank
secret: rather-secret
# The URL to get the health status
status: http://localhost:8040/healthz
# The URL to get playlist details via Tank
playlist: http://localhost:8040/api/v1/playlists/${ID}
engine:
# Engine ID (1 or 2)
number: 1
# Engine API availability check
status: http://localhost:8008/api/v1/ui/
# Engine API endpoint to store playlogs
store_playlog: http://localhost:8008/api/v1/playlog
# Engine API endpoint to store clock information
store_clock: http://localhost:8008/api/v1/clock
# Engine API endpoint to store health information
store_health: http://localhost:8008/api/v1/source/health/${ENGINE_NUMBER}
scheduler:
# Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs)
# Either provide an absolute base path or a relative one starting in the `engine` directory. In case of `engine-core` running in docker use `/var/audio/source`
audio:
source_folder: ../engine-core/audio/source
source_extension: .flac
# Folder holding M3U Playlists to be scheduled in form of Engine Playlists (similar as audio source folder above)
playlist_folder: ../engine-core/audio/playlist
# Offset in seconds how long it takes for Liquidsoap to actually execute a scheduler command; Crucial to keep things in sync
engine_latency_offset: 0.5
# How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show is aired
fetching_frequency: 30
# The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.
scheduling_window_start: 60
scheduling_window_end: 60
# How many seconds before the actual schedule time the entry should be pre-loaded. Note to provide enough timeout for
# contents which take longer to load (big files, bad connectivity to streams etc.). If the planned start time is in
# the past the offset is ignored and the entry is played as soon as possible
preload_offset: 15
# Sometimes it might take longer to get a stream connected. Here you can define a viable length.
# But note, that this may affect the preloading time (see `preload_offset`), hence affecting the
# overall playout, its delays and possible fallbacks
input_stream:
buffer: 3.0
# Fade duration when selecting another mixer input (seconds)
fade_in_time: 1.5
fade_out_time: 1.5
[program:aura-engine]
user = engineuser
directory = /opt/aura/engine
command = /opt/aura/engine/make run
priority = 666
autostart = true
autorestart = true
stopsignal = TERM
redirect_stderr = true
stdout_logfile = /opt/aura/engine/logs/engine-stdout.log
stderr_logfile = /opt/aura/engine/logs/engine-error.log
\ No newline at end of file
[Unit]
Description=AURA Engine - Scheduler
Documentation=https://gitlab.servus.at/aura/engine
After=network.target
Requires=aura-engine-core.service
[Service]
Type=simple
User=engineuser
WorkingDirectory=/opt/aura/engine
ExecStart=/opt/aura/engine/make run
Restart=always
[Install]
WantedBy=multi-user.target
Alias=aura-engine.service
\ No newline at end of file
#!/usr/bin/env python3
# Copyright (c) 2001, Nicola Larosa
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of the <ORGANIZATION> nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""PyHeartBeat server: receives and tracks UDP packets from all clients.
While the BeatLog thread logs each UDP packet in a dictionary, the main
thread periodically scans the dictionary and prints the IP addresses of the
clients that sent at least one packet during the run, but have
not sent any packet since a time longer than the definition of the timeout.
Adjust the constant parameters as needed, or call as:
PyHeartBeat.py [udpport [timeout]]
Set the environment variable "DEBUG" to "1" in order to emit more detailed
debug messages.
In addition "127.0.0.1" is marked as a previously active peer.
Manual heartbeat messages can be easily sent via "netcat":
echo foo | nc -q 1 -u localhost 43334
https://www.oreilly.com/library/view/python-cookbook/0596001673/ch10s13.html
"""
import os
import socket
import sys
from threading import Event, Lock, Thread
from time import ctime, sleep, time
DEFAULT_HEARTBEAT_PORT = 43334
DEFAULT_WAIT_PERIOD = 10
DEBUG_ENABLED = os.getenv("DEBUG", "0") == "1"
class BeatDict:
"""Manage heartbeat dictionary."""
def __init__(self):
self.beatDict = {}
if DEBUG_ENABLED:
self.beatDict["127.0.0.1"] = time()
self.dictLock = Lock()
def __repr__(self):
result = ""
self.dictLock.acquire()
for key in self.beatDict.keys():
result += "IP address: %s - Last time: %s\n" % (
key,
ctime(self.beatDict[key]),
)
self.dictLock.release()
return result
def update(self, entry):
"""Create or update a dictionary entry."""
self.dictLock.acquire()
self.beatDict[entry] = time()
self.dictLock.release()
def extractSilent(self, howPast):
"""Return a list of entries older than howPast."""
silent = []
when = time() - howPast
self.dictLock.acquire()
for key in self.beatDict.keys():
if self.beatDict[key] < when:
silent.append(key)
self.dictLock.release()
return silent
class BeatRec(Thread):
"""Receive UDP packets, log them in heartbeat dictionary."""
def __init__(self, goOnEvent, updateDictFunc, port):
Thread.__init__(self)
self.goOnEvent = goOnEvent
self.updateDictFunc = updateDictFunc
self.port = port
self.recSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.recSocket.settimeout(0.2)
self.recSocket.bind(("", port))
def __repr__(self):
return f"Heartbeat Server on port: {self.port}"
def run(self):
"""Start the beat receiver."""
while self.goOnEvent.isSet():
if DEBUG_ENABLED:
print("Waiting to receive...")
try:
data, addr = self.recSocket.recvfrom(6)
except socket.timeout:
# no incoming message -> no timestamp update -> check again
pass
else:
if DEBUG_ENABLED:
print(f"Received packet from {addr}")
self.updateDictFunc(addr[0])
def main():
"""Listen to the heartbeats and detect inactive clients."""
if len(sys.argv) > 1:
heartbeat_port = int(sys.argv[1])
else:
heartbeat_port = DEFAULT_HEARTBEAT_PORT
if len(sys.argv) > 2:
wait_period = float(sys.argv[2])
else:
wait_period = DEFAULT_WAIT_PERIOD
beatRecGoOnEvent = Event()
beatRecGoOnEvent.set()
beatDictObject = BeatDict()
beatRecThread = BeatRec(beatRecGoOnEvent, beatDictObject.update, heartbeat_port)
if DEBUG_ENABLED:
print(beatRecThread)
beatRecThread.start()
print(f"PyHeartBeat server listening on port {heartbeat_port}")
print("\n*** Press Ctrl-C to stop ***\n")
while True:
try:
if DEBUG_ENABLED:
print(f"Beat Dictionary: {beatDictObject}")
silent = beatDictObject.extractSilent(wait_period)
if silent:
print(f"Silent clients: {' '.join(silent)}")
sleep(wait_period)
except KeyboardInterrupt:
print("Exiting.")
beatRecGoOnEvent.clear()
beatRecThread.join()
break
if __name__ == "__main__":
main()
# Aura Engine Development Guide
This page gives insights on extending Aura Engine internals or through the API.
<!-- TOC -->
1. [AURA Components](#aura-components)
2. [Engine Components](#engine-components)
3. [Running for Development](#running-for-development)
4. [Testing](#testing)
5. [API](#api)
6. [Scheduler](#scheduler)
7. [Daemonized Engine](#daemonized-engine)
1. [Running with Systemd](#running-with-systemd)
2. [Running with Supervisor](#running-with-supervisor)
8. [Logging](#logging)
9. [Read more](#read-more)
<!-- /TOC -->
## AURA Components
AURA Engine as part of the AURA Radio Suite uses an modulear architecture based on a REST API. All external information is retrieved using JSON data-structures.
To get the basic architectural overview, visit [docs.aura.radio](https://docs.aura.radio).
Starting development of engine can be quite tedious, as it requires all most all other AURA components to be up and running.
For example:
- Steering, to get the main incredient of an play-out engine: schedules (or "timeslots" in Steering terms),
which hold the actual information on playlists and their items.
- Dashboard, to have a neat interface, being able to program the timeslots
- Tank, to get the references to audio files and other audio sources. Plus the actual files.
If you need to test and develop against the Engine's API you'll also need to get the `engine-api` project running.
For a start it is recommended to create a general `aura` project folder. In there you start cloning all the sub-projects. After having all the sub-projects configured, and verified that they are working, take a look at the `aura/aura` repository.
## Engine Components
`TODO: add content`
## Running for Development
Ensure you have following other projects up and running:
- steering
- tank
- dashboard
- engine-api
- dashboard-clock (optional)
The following steps expect you already have performed basic Engine configuration, as outlined in the [Native Installation](https://gitlab.servus.at/aura/engine/-/blob/main/docs/bare-metal-installation.md) document.
Start Liquidsoap which is part of Engine Core:
```shell
~/code/aura/engine-core$ make run
```
Now run the Engine:
```shell
~/code/aura/engine$ make run
```
If your IDE of choice is _Visual Studio Code_, then there are launch settings provided in `.vscode/launch.json`.
## Testing
Test cases are located in `./tests` are executed by running:
```shell
~/code/aura/engine$ make test
```
## API
You can find the AURA API definition at https://api.aura.radio
Engine utilizes the Steering API and Tank API to query scheduling and playlist data.
The OpenAPI specification for these APIs are located in `schemas` and used for the client models.
To generate the API client models run
```bash
make api
```
Find the generated models in the packages `aura_steering_api` and `aura_tank_api`.
## Scheduler
Scheduling is split into multiple phases. Below you see a timeline with one timeslot planned at a certain
point in time and the involved phase before:
```ascii
========================================= [ Scheduling Window ] ===========
======================================================= [ Timeslot Play-out ] ====
== (FILESYSTEM A) ========================== [ Preload ] [ Play 4 ] ====================================
== (STREAM A) ========================================== [ Preload ] [ Play 1 ] ==========================
== (LIVE 1) ====================================================== [ Preload ] [ Play 1 ] ================
== (FILESYSTEM B) ========================================================== [ Preload ] [ Play 4 ] ====
```
- **Scheduling Window**: Within the scheduling window any commands for controlling
the mixer of the soundsystem are prepared and queued.
Only until the start of the window, timeslots can be updated or removed via external API Endpoints
(e.g. using Steering or Dashboard). Until here any changes on the timeslot itself will be reflected
in the actual play-out. This only affects the start and end time of the "timeslot" itself.
It does not involve related playlists and their items. Those can still be modified after the
scheduling window has started.
The start and the end of the window is defined by the start of the timeslot minus
a configured amount of seconds (see `scheduling_window_start` and `scheduling_window_end`
in `engine.yaml`). The actual start of the window is calculated by (timeslot start - window start)
and the end by (timeslot end - window end)
During the scheduling window, the external API Endpoints are pulled continuously, to
check for updated timeslots and related playlists. Also, any changes to playlists and
its items are respected within that window (see `fetching_frequency` in `engine.yaml`).
> Important: It is vital that the the scheduling window is wider than the fetching frequency.
> Otherwise one fetch might never hit a scheduling window, hence not being able to schedule stuff.
> Note: If you delete any existing timeslot in Dashboard/Steering this is only reflected in Engine until the start
> of the scheduling window. The scheduling window is defined by the start of the timeslot minus a configured offset
> in seconds. This is limitation is required to avoid corrupted playout in case audio content has been
> preloaded or started playing already.
- **Queuing and Pre-Loading**: Before any playlist items of the timeslot can be turned into
sound, they need to be queued and pre-loaded. Ideally the pre-loading happens somewhat before
the scheduled play-out time to avoid any delays in timing. Set the maximum time reserved for
pre-loading in your configuration (compare `preload_offset`in `engine.yaml`).
If there is not enough time to reserve the given amount of time for preloading (i.e. some playlist item
should have started in the past already) the offset is ignored and the item is played as soon as possible.
> Important: To ensure proper timings, the offset should not exceed the time between the start of
> the scheduling-window and the start of the actual timeslot playout. Practically, of course there
> are scenario where playout start later than planned e.g. during startup of the engine during a timeslot
> or due to some severe connectivity issues to some external stream.
- **Play-out**: Finally the actual play-out is happening. The faders of the virtual mixers are pushed
all the way up, as soon it is "time to play" for one of the pre-loaded items.
Transitions between playlist items with different types of sources (file, stream and line
inputs) are performed automatically. At the end of each timeslot the channel is faded-out,
no matter if the total length of the playlist items would require a longer timeslot.
If for some reason the playout is corrupted, stopped or too silent to make any sense, then
this <u>triggers a fallback using the silence detector</u>.
## Daemonized Engine
For this you can utilize either [Systemd](https://systemd.io/) or [Supervisor](http://supervisord.org/). Please check the their manuals on how to use these services.
The daemon configs are expecting you run engine under the user `engineuser` and being located under `/opt/aura/engine`, `/opt/aura/engine-api` and `/opt/aura/engine-core` respectively. Do prepare the project root and permissions you can use the script `scripts/initialize-systemd.sh`. To create the matching user and audio group membership run `scripts/create-engineuser.sh`.
### Running with Systemd
Copy the unit files in `/opt/aura/engine/config/systemd/aura-engine.service` to your systemd unit directory, and reload the systemd daemon:
```shell
cp /opt/aura/engine/config/systemd/* /etc/systemd/system/
systemctl daemon-reload
```
### Running with Supervisor
Now, given you are in the engine's home directory like `/opt/aura/engine/`, simply type following to start the services:
```shell
supervisord
```
This picks up the supervisor configuration provided in the local `supervisord.conf` and the service configurations located in `config/supervisor/*.conf`.
Then you'll need to reload the supervisor configuration using `sudo`:
```shell
sudo supervisorctl reload
```
## Logging
All Engine logs can be found under `./logs`.
## Read more
- [Bare Metal Installation](docs/bare-metal-installation.md)
- [Developer Guide](docs/developer-guide.md)
- [Setting up the Audio Store [docs.aura.radio]](https://docs.aura.radio/en/latest/administration/setup-audio-store.html)
# Ignore everything in this directory
*
# Except this file
!.gitignore
\ No newline at end of file
This diff is collapsed.
[tool.black]
line-length = 99
target-version = ["py310"]
exclude = '''
'''
[tool.isort]
profile = "black"
[tool.pytest.ini_options]
pythonpath = ["src"]
[tool.poetry]
name = "aura-engine"
version = "1.0.0-alpha5"
description = "AURA Engine scheduling and playout control."
license = "AGPL-3.0-or-later"
authors = [
"David Trattnig <david@subsquare.at>",
"Gottfried Gaisbauer <gogo@servus.at>",
]
maintainers = [
"Christoph Pastl <christoph.pastl@fro.at>"
]
readme = "README.md"
homepage = "https://aura.radio/"
repository = "https://gitlab.servus.at/aura/engine"
documentation = "https://docs.aura.radio/"
keywords = ["radio", "scheduling", "audio", "playout"]
classifiers = [
"Topic :: Multimedia :: Sound/Audio",
"Topic :: Multimedia :: Sound/Audio :: Mixers",
"Topic :: Multimedia :: Sound/Audio :: Players",
]
packages = [{ include = "aura_engine", from = "src" }]
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.24.0"
itsdangerous = "2.0.1"
openapi-python-client = "^0.14.0"
flake8-bandit = "^4.1.1"
tomli = "^2.0.1"
confuse = "^2.0.1"
jsonpickle = "^3.0.2"
[tool.poetry.group.dev.dependencies]
codespell = "^2.2.1"
black = "^23.3.0"
flake8 = "^5.0.4"
flake8-docstrings = "^1.6.0"
validators = "^0.20.0"
isort = "^5.10.1"
pre-commit = "^3.6.2"
coverage = {extras = ["toml"], version = "^7.2.5"}
[tool.coverage.run]
source = ["src"]
omit = ["types.py"]
[tool.coverage.report]
# TODO Increase after we have more test cases
fail_under = 40
exclude_lines = [
"if __name__ == .__main__.:",
]
[build-system]
requires = ["poetry>=1.3"]
build-backend = "poetry.masonry.api"
TZ=Europe/Vienna
# Possible values: debug, info, warning, error, critical
AURA_ENGINE_LOG_LEVEL=info
# API
AURA_ENGINE_API_BASE_URL=http://127.0.0.1:8008/
AURA_STEERING_BASE_URL=http://127.0.0.1:8000/
AURA_TANK_BASE_URL=http://127.0.0.1:8040/
AURA_TANK_ENGINE_USER=engine
AURA_TANK_ENGINE_PASSWORD=rather-secret
# Offset in seconds how long it takes for Liquidsoap to actually execute a scheduler command; Crucial to keep things in sync
AURA_ENGINE_LATENCY_OFFSET=0.5
# Fade duration when selecting another mixer input (seconds)
AURA_ENGINE_FADE_IN_TIME=1.5
AURA_ENGINE_FADE_OUT_TIME=1.5
# Host and UDP port where heartbeat is sent to (disabled if empty string)
# Seconds how often the vitality of Engine Core should be checked (default=1)
AURA_ENGINE_HEARTBEAT_SERVER=
AURA_ENGINE_HEARTBEAT_SERVER_PORT=43334
AURA_ENGINE_HEARTBEAT_FREQUENCY=1
{
"openapi": "3.0.3",
"info": {
"title": "AURA Steering API",
"version": "1.0.0",
"description": "Programme/schedule management for Aura"
},
"paths": {},
"components": {
"schemas": {
"PlayoutEpisode": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"readOnly": true
},
"title": {
"type": "string",
"description": "Title of the note.",
"maxLength": 128
}
},
"required": ["id"]
},
"PlayoutProgramEntry": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"start": {
"type": "string",
"format": "date-time"
},
"end": {
"type": "string",
"format": "date-time"
},
"timeslotId": {
"type": "integer",
"nullable": true
},
"playlistId": {
"type": "integer",
"nullable": true
},
"showId": {
"type": "integer"
},
"timeslot": {
"allOf": [
{
"$ref": "#/components/schemas/TimeSlot"
}
],
"nullable": true
},
"show": {
"$ref": "#/components/schemas/PlayoutShow"
},
"episode": {
"allOf": [
{
"$ref": "#/components/schemas/PlayoutEpisode"
}
],
"nullable": true
},
"schedule": {
"allOf": [
{
"$ref": "#/components/schemas/PlayoutSchedule"
}
],
"nullable": true
}
},
"required": [
"end",
"episode",
"id",
"playlistId",
"schedule",
"show",
"showId",
"start",
"timeslot",
"timeslotId"
]
},
"PlayoutSchedule": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"readOnly": true
},
"defaultPlaylistId": {
"type": "integer",
"nullable": true,
"description": "A tank ID in case the timeslot's playlist_id is empty."
}
},
"required": ["id"]
},
"PlayoutShow": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"readOnly": true
},
"name": {
"type": "string",
"description": "Name of this Show.",
"maxLength": 255
},
"defaultPlaylistId": {
"type": "integer",
"nullable": true
}
},
"required": ["id", "name"]
},
"TimeSlot": {
"type": "object",
"properties": {
"memo": {
"type": "string",
"description": "Memo for this timeslot."
},
"playlistId": {
"type": "integer",
"nullable": true,
"description": "Playlist ID of this timeslot."
},
"repetitionOfId": {
"type": "integer",
"nullable": true,
"description": "This timeslot is a repetition of `Timeslot` ID."
},
"end": {
"type": "string",
"format": "date-time",
"readOnly": true
},
"id": {
"type": "integer",
"readOnly": true
},
"noteId": {
"type": "integer",
"readOnly": true
},
"scheduleId": {
"type": "integer",
"description": "`Schedule` ID of this timeslot."
},
"showId": {
"type": "integer",
"readOnly": true
},
"start": {
"type": "string",
"format": "date-time",
"readOnly": true
}
},
"required": ["end", "id", "noteId", "showId", "start"]
}
}
}
}
{
"openapi": "3.0.1",
"info": {
"contact": {
"name": "aura.radio",
"url": "https://aura.radio"
},
"description": "Import & Playlist Daemon",
"license": {
"name": "AGPLv3",
"url": "https://www.gnu.org/licenses/agpl-3.0"
},
"title": "AURA Tank API",
"version": "1.0"
},
"servers": [
{
"url": "/"
}
],
"paths": {},
"components": {
"schemas": {
"File": {
"properties": {
"created": {
"type": "string"
},
"duration": {
"type": "number"
},
"id": {
"type": "integer"
},
"metadata": {
"$ref": "#/components/schemas/FileMetadata"
},
"showName": {
"type": "string"
},
"size": {
"type": "integer"
},
"source": {
"$ref": "#/components/schemas/FileSource"
},
"updated": {
"type": "string"
}
},
"type": "object"
},
"FileMetadata": {
"properties": {
"album": {
"type": "string"
},
"artist": {
"description": "actually a full-text index would be nice here...",
"type": "string"
},
"isrc": {
"type": "string"
},
"organization": {
"type": "string"
},
"title": {
"type": "string"
}
},
"type": "object"
},
"FileSource": {
"properties": {
"hash": {
"type": "string"
},
"import": {
"$ref": "#/components/schemas/Import"
},
"uri": {
"type": "string"
}
},
"type": "object"
},
"Import": {
"properties": {
"error": {
"type": "string"
},
"state": {
"type": "string"
}
},
"type": "object"
},
"Playlist": {
"properties": {
"created": {
"type": "string"
},
"description": {
"type": "string"
},
"entries": {
"items": {
"$ref": "#/components/schemas/PlaylistEntry"
},
"type": "array"
},
"id": {
"type": "integer"
},
"playoutMode": {
"type": "string"
},
"showName": {
"type": "string"
},
"updated": {
"type": "string"
}
},
"type": "object"
},
"PlaylistEntry": {
"properties": {
"duration": {
"type": "number"
},
"file": {
"$ref": "#/components/schemas/File"
},
"uri": {
"type": "string"
}
},
"type": "object"
}
},
"x-original-swagger-version": "2.0"
}
}
\ No newline at end of file
#!/bin/bash
if getent passwd 'engineuser' > /dev/null 2>&1; then
echo "User 'engineuser' exists already.";
else
echo "Creating Engine User ..."
adduser engineuser
adduser engineuser audio
fi
\ No newline at end of file
#!/bin/bash
#
# Prepare folders and permissions for installing engine on production.
#
# You'll need sudo/root privileges.
#
echo "Set Ownership of '/opt/aura/engine', '/var/log/aura/' and '/etc/aura/engine.yaml' to Engine User"
chown -R engineuser:engineuser /opt/aura
chown -R engineuser:engineuser /etc/aura
chown -R engineuser:engineuser /var/log/aura
chown -R engineuser:engineuser /var/log/supervisor
echo "Copy Systemd unit files to '/etc/systemd/system/'"
cp -n /opt/aura/engine/config/systemd/* /etc/systemd/system/
\ No newline at end of file