my eye

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>