Bootstrap
Committed abb2b9
index 0000000..e38eefc
--- /dev/null
+name: Run Tests and Analysis
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build-linux:
+ strategy:
+ matrix:
+ python-version: ["3.10"]
+ runs-on: "ubuntu-latest"
+ steps:
+ - name: Install graphviz
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y graphviz
+
+ - uses: actions/checkout@v3
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Remove development dependencies
+ run: sed -i '/\[tool.poetry.group.dev.dependencies\]/,/\[/d' pyproject.toml
+
+ - name: Install Poetry
+ uses: snok/install-poetry@v1
+ with:
+ version: 1.2.2
+ virtualenvs-in-project: true
+
+ - name: Install dependencies
+ run: poetry install --no-interaction --no-root
+
+ - name: Install library
+ run: poetry install --no-interaction
+
+ - name: Install development tools
+ run: poetry add gmpg
+
+ - uses: psf/black@stable
+ with:
+ options: "--check --verbose"
+ src: "."
+ version: "23.7"
+
+ - uses: isort/isort-action@v1
+ with:
+ configuration: "--profile black"
+
+ # - name: Run tests
+ # run: poetry run gmpg test
+
+ - name: Run analysis
+ run: poetry run gmpg analyze
+
+ - name: Generate dependency graph
+ run: poetry run gmpg graph
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: analysis
+ path: |
+ test_coverage.xml
+ test_results.xml
+ api_python.json
+ deps.svg
index 0000000..74d1111
--- /dev/null
+[tool.poetry]
+name = "webint-live"
+version = "0.0.22"
+description = "stream from your website"
+authors = ["Angelo Gladding <angelo@ragt.ag>"]
+license = "AGPL-3.0-or-later"
+packages = [{include = "webint_live"}]
+
+[tool.poetry.plugins."webapps"]
+live = "webint_live:app"
+
+[tool.poetry.dependencies]
+python = ">=3.10,<3.11"
+webint = ">=0.0"
+
+[tool.poetry.group.dev.dependencies]
+# gmpg = "^0.0"
+bgq = {path="../bgq", develop=true}
+gmpg = {path="../gmpg", develop=true}
+newmath = {path="../newmath", develop=true}
+sqlyte = {path="../sqlyte", develop=true}
+webagt = {path="../webagt", develop=true}
+webint = {path="../webint", develop=true}
+
+# [[tool.poetry.source]]
+# name = "main"
+# url = "https://ragt.ag/code/pypi"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
index 0000000..57612a4
--- /dev/null
+"""Stream from your website."""
+
+import web
+
+app = web.application(__name__, prefix="live")
+
+
+@app.control("")
+class Live:
+ """Live stream and chat."""
+
+ def get(self):
+ """Return both stream and chat."""
+ return app.view.index()
+
+
+@app.control("stream")
+class Stream:
+ """Stream to consumers (listeners/viewers)."""
+
+ def get(self):
+ """Return a live stream (RTMP)."""
+ return app.view.stream()
+
+
+@app.control("chat")
+class Chat:
+ """Chat with guests."""
+
+ def get(self):
+ """Return a video chat ([Web]RTC)."""
+ return app.view.chat()
index 0000000..5264c66
--- /dev/null
+from pprint import pformat
+
+from web import tx
+
+__all__ = ["pformat", "tx"]
index 0000000..2078407
--- /dev/null
+$def with ()
+$var title: Chat
+
+$# <p>...</p>
+
+$# <iframe style=height:60vh;width:100%
+$# src=https://v3demo.mediasoup.org/?roomId=ragtag
+$# allow=display-capture;camera;microphone></iframe>
+
+<script src=https://cdn.jsdelivr.net/npm/webtorrent@latest/webtorrent.min.js></script>
+
+$# <script type=module>
+$# const client = new WebTorrent()
+$# const torrentId = 'magnet:?xt=urn:btih:2111e0cf5edb0b526eb5e0061b84f72275ad3a7d&dn=2023-05-20+13-53-10.mkv&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com'
+$# client.add(torrentId, function (torrent) {
+$# const file = torrent.files.find(function (file) {
+$# return file.name.endsWith('.mkv')
+$# })
+$# console.log(file)
+$# file.getBlobURL(function (err, url) {
+$# if (err) return console.log(err.message)
+$# console.log('File done.')
+$# console.log('<a href="' + url + '">Download full file: ' + file.name + '</a>')
+$# })
+$# })
+$# </script>
+
+<form id=torr>
+ <label for="torrentId">Download from a magnet link: </label>
+ <input name="torrentId", placeholder="magnet:" value="">
+ <button type="submit">Download</button>
+</form>
+
+<h2>Log</h2>
+<div class="log"></div>
+
+<script>
+ const client = new WebTorrent()
+
+ client.on('error', function (err) {
+ console.error('ERROR: ' + err.message)
+ })
+
+ document.querySelector('form#torr').addEventListener('submit', function (e) {
+ e.preventDefault() // Prevent page refresh
+
+ const torrentId = document.querySelector('form input[name=torrentId]').value
+ log('Adding ' + torrentId)
+ client.add(torrentId, onTorrent)
+ })
+
+ function onTorrent (torrent) {
+ log('Got torrent metadata!')
+ log(
+ 'Torrent info hash: ' + torrent.infoHash + ' ' +
+ '<a href="' + torrent.magnetURI + '" target="_blank">[Magnet URI]</a> ' +
+ '<a href="' + torrent.torrentFileBlobURL + '" target="_blank" download="' + torrent.name + '.torrent">[Download .torrent]</a>'
+ )
+
+ // Print out progress every 5 seconds
+ const interval = setInterval(function () {
+ log('Progress: ' + (torrent.progress * 100).toFixed(1) + '%')
+ }, 5000)
+
+ torrent.on('done', function () {
+ log('Progress: 100%')
+ clearInterval(interval)
+ })
+
+ // Render all files into to the page
+ torrent.files.forEach(function (file) {
+ file.appendTo('.log')
+ log('(Blob URLs only work if the file is loaded from a server. "http//localhost" works. "file://" does not.)')
+ file.getBlobURL(function (err, url) {
+ if (err) return log(err.message)
+ log('File done.')
+ log('<a href="' + url + '">Download full file: ' + file.name + '</a>')
+ })
+ })
+ }
+
+ function log (str) {
+ const p = document.createElement('p')
+ p.innerHTML = str
+ document.querySelector('.log').appendChild(p)
+ }
+</script>
index 0000000..05d5029
--- /dev/null
+$def with (micropub_endpoint, access_token, file_root)
+#!/bin/bash
+
+micropub_endpoint=https://ragt.ag/posts
+access_token=secret-token:H/_MnZxm795q9.9bNLI//zqK3ex1wl94_+SKTNCwFqP_~/_i9wUmDJSYM_OE+PCwSMxP..mCJj7YV+iE~CtcH.cfsitG_RkibB3IDDUz.74oPEuKU_lPy60NF+pdsE.N
+file_root="/root/ragt.ag"
+url=`cat $file_root/last-url.txt`
+
+input_file=$1
+video_filename=$2
+output=$file_root/archive/$2;
+
+curl -X POST -i $micropub_endpoint -H "Authorization: Bearer $access_token" \
+ -H "Content-Type: application/json" \
+ -d "{\"action\":\"update\",\"url\":\"$url\",\"replace\":{\"content\":\"<p>The live stream has ended. The archived version will be available here shortly.</p>\"}}"
+
+ffmpeg -y -i $input_file -acodec libmp3lame -ar 44100 -ac 1 -vcodec libx264 $output;
+video_url="/live/archive/$video_filename"
+
+ffmpeg -i $output -vf "thumbnail,scale=1920:1080" -frames:v 1 $output.jpg
+photo_url="/archive/$video_filename.jpg"
+
+curl -X POST -i $micropub_endpoint -H "Authorization: Bearer $access_token" \
+ -H "Content-Type: application/json" \
+ -d "{\"action\":\"update\",\"url\":\"$url\",\"replace\":{\"content\":\"<p>The live stream has ended.</p>\"},\"add\":{\"video\":\"$video_url\",\"photo\":\"$photo_url\"}}"
index 0000000..36bb1cb
--- /dev/null
+$def with ()
+$var title: Live
+
+<p>...</p>
index 0000000..ee08541
--- /dev/null
+$def with (micropub_endpoint, access_token, file_root)
+#!/bin/bash
+
+micropub_endpoint=https://ragt.ag/posts
+access_token=secret-token:H/_MnZxm795q9.9bNLI//zqK3ex1wl94_+SKTNCwFqP_~/_i9wUmDJSYM_OE+PCwSMxP..mCJj7YV+iE~CtcH.cfsitG_RkibB3IDDUz.74oPEuKU_lPy60NF+pdsE.N
+file_root="/root/ragt.ag"
+
+curl -X POST -i $micropub_endpoint -H "Authorization: Bearer $access_token" \
+ -H "Content-Type: application/json" \
+ -d '{"type": ["h-entry"], "properties": {"content": {"html": "<p>Streaming Live</p><iframe width=640 height=360 src=/live></iframe>"}, "visibility": ["public"]}}' | grep location: | sed -En 's/^location: (.+)/\1/p' | tr -d '\r\n' > $file_root/last-url.txt
index 0000000..7b90e1e
--- /dev/null
+$def with ()
+$var title: Live Stream
+
+<link href=https://vjs.zencdn.net/5.8.8/video-js.css rel=stylesheet>
+<script src=https://vjs.zencdn.net/5.8.8/video.js></script>
+<script src=https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-hls/3.6.12/videojs-contrib-hls.js></script>
+<div id=stream></div>
+<script>
+fetch('/stream-stats')
+ .then(response => response.text())
+ .then(str => new window.DOMParser().parseFromString(str, "text/xml"))
+ .then(data => {
+ const streams = data.getElementsByTagName('stream')
+ if (streams.length) {
+ document.querySelector('#stream').innerHTML = `
+ <video id=livestream class="video-js vjs-default-skin" controls>
+ <source src=$tx.origin/hls/foo.m3u8 type=application/x-mpegURL>
+ </video>
+ <div style=font-size:.75em>started <span id=streamduration></span> minutes ago</div>
+ `
+ player = videojs('livestream', {fluid: true})
+ player.play()
+ // document.querySelector('#watching').innerHTML = streams[0].getElementsByTagName('nclients')[0].textContent
+ document.querySelector('#streamduration').innerHTML = Math.round(parseInt(streams[0].getElementsByTagName('time')[0].textContent) / 60000)
+ }
+ })
+</script>