コミット
5d0cac0b0f
|
@ -1 +1,2 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
stream/
|
||||||
|
|
|
@ -6,6 +6,7 @@ from quart import Quart
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
from anonstream.utils.token import generate_token
|
from anonstream.utils.token import generate_token
|
||||||
|
from anonstream.segments import DirectoryCache
|
||||||
|
|
||||||
async def create_app():
|
async def create_app():
|
||||||
with open('config.toml') as fp:
|
with open('config.toml') as fp:
|
||||||
|
@ -17,12 +18,13 @@ async def create_app():
|
||||||
print('Broadcaster password:', auth_password)
|
print('Broadcaster password:', auth_password)
|
||||||
|
|
||||||
app = Quart('anonstream')
|
app = Quart('anonstream')
|
||||||
app.config['SECRET_KEY'] = config['secret'].encode()
|
app.config['SECRET_KEY'] = config['secret_key'].encode()
|
||||||
app.config['AUTH_USERNAME'] = config['auth_username']
|
app.config['AUTH_USERNAME'] = config['auth_username']
|
||||||
app.config['AUTH_PWHASH'] = auth_pwhash
|
app.config['AUTH_PWHASH'] = auth_pwhash
|
||||||
app.config['AUTH_TOKEN'] = generate_token()
|
app.config['AUTH_TOKEN'] = generate_token()
|
||||||
app.chat = OrderedDict()
|
app.chat = OrderedDict()
|
||||||
app.websockets = {}
|
app.websockets = {}
|
||||||
|
app.segments_directory_cache = DirectoryCache(config['segments_dir'])
|
||||||
|
|
||||||
async with app.app_context():
|
async with app.app_context():
|
||||||
import anonstream.routes
|
import anonstream.routes
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from quart import current_app, request, render_template, redirect, websocket
|
from quart import current_app, request, render_template, make_response, redirect, websocket
|
||||||
|
|
||||||
|
from anonstream.segments import CatSegments
|
||||||
from anonstream.wrappers import with_token_from, auth_required
|
from anonstream.wrappers import with_token_from, auth_required
|
||||||
from anonstream.websocket import websocket_outbound, websocket_inbound
|
from anonstream.websocket import websocket_outbound, websocket_inbound
|
||||||
|
|
||||||
|
@ -10,6 +11,18 @@ from anonstream.websocket import websocket_outbound, websocket_inbound
|
||||||
async def home(token):
|
async def home(token):
|
||||||
return await render_template('home.html', token=token)
|
return await render_template('home.html', token=token)
|
||||||
|
|
||||||
|
@current_app.route('/stream.mp4')
|
||||||
|
@with_token_from(request)
|
||||||
|
async def stream(token):
|
||||||
|
try:
|
||||||
|
cat_segments = CatSegments(current_app.segments_directory_cache, token)
|
||||||
|
except ValueError:
|
||||||
|
return 'offline', 404
|
||||||
|
response = await make_response(cat_segments.stream())
|
||||||
|
response.headers['Content-Type'] = 'video/mp4'
|
||||||
|
response.timeout = None
|
||||||
|
return response
|
||||||
|
|
||||||
@current_app.route('/login')
|
@current_app.route('/login')
|
||||||
@auth_required
|
@auth_required
|
||||||
async def login():
|
async def login():
|
||||||
|
@ -23,10 +36,10 @@ async def live(token):
|
||||||
|
|
||||||
producer = websocket_outbound(queue)
|
producer = websocket_outbound(queue)
|
||||||
consumer = websocket_inbound(
|
consumer = websocket_inbound(
|
||||||
secret=current_app.config['SECRET_KEY'],
|
|
||||||
connected_websockets=current_app.websockets,
|
connected_websockets=current_app.websockets,
|
||||||
chat=current_app.chat,
|
|
||||||
token=token,
|
token=token,
|
||||||
|
secret=current_app.config['SECRET_KEY'],
|
||||||
|
chat=current_app.chat,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await asyncio.gather(producer, consumer)
|
await asyncio.gather(producer, consumer)
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
|
RE_SEGMENT = re.compile(r'^(?P<index>\d+)\.ts$')
|
||||||
|
|
||||||
|
class DirectoryCache:
|
||||||
|
def __init__(self, directory, ttl=0.5):
|
||||||
|
self.directory = directory
|
||||||
|
self.ttl = ttl
|
||||||
|
self.expires = None
|
||||||
|
self.files = None
|
||||||
|
|
||||||
|
def timer(self):
|
||||||
|
return time.monotonic()
|
||||||
|
|
||||||
|
def listdir(self):
|
||||||
|
if self.expires is None or self.timer() >= self.expires:
|
||||||
|
print(f'[debug @ {time.time():.4f}] listdir()')
|
||||||
|
self.files = os.listdir(self.directory)
|
||||||
|
self.expires = self.timer() + self.ttl
|
||||||
|
return self.files
|
||||||
|
|
||||||
|
def segments(self):
|
||||||
|
segments = []
|
||||||
|
for fn in self.listdir():
|
||||||
|
match = RE_SEGMENT.match(fn)
|
||||||
|
if match:
|
||||||
|
segments.append((int(match.group('index')), fn))
|
||||||
|
segments.sort()
|
||||||
|
return OrderedDict(segments)
|
||||||
|
|
||||||
|
def path(self, fn):
|
||||||
|
return os.path.join(self.directory, fn)
|
||||||
|
|
||||||
|
class CatSegments:
|
||||||
|
def __init__(self, directory_cache, token):
|
||||||
|
self.directory_cache = directory_cache
|
||||||
|
self.token = token
|
||||||
|
self.index = max(self.directory_cache.segments())
|
||||||
|
|
||||||
|
async def stream(self):
|
||||||
|
while True:
|
||||||
|
print(
|
||||||
|
f'[debug @ {time.time():.4f}: {self.token}] '
|
||||||
|
f'index={self.index} '
|
||||||
|
f'segments={tuple(self.directory_cache.segments())}'
|
||||||
|
)
|
||||||
|
# search for current segment
|
||||||
|
for i in range(21):
|
||||||
|
segment = self.directory_cache.segments().get(self.index)
|
||||||
|
if segment is not None:
|
||||||
|
break
|
||||||
|
if i != 20:
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f'[debug @ {time.time():.4f}: {self.token}] could not '
|
||||||
|
f'find segment #{self.index} after at least 4 seconds'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# read current segment
|
||||||
|
fn = self.directory_cache.path(segment)
|
||||||
|
async with aiofiles.open(fn, 'rb') as fp:
|
||||||
|
while chunk := await fp.read(8192):
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
# increment segment index
|
||||||
|
self.index += 1
|
|
@ -112,18 +112,21 @@ const connect_websocket = () => {
|
||||||
websocket_backoff = 2000; // 2 seconds
|
websocket_backoff = 2000; // 2 seconds
|
||||||
});
|
});
|
||||||
ws.addEventListener("close", (event) => {
|
ws.addEventListener("close", (event) => {
|
||||||
|
console.log("websocket closed", event);
|
||||||
chat_form_submit.disabled = true;
|
chat_form_submit.disabled = true;
|
||||||
chat_live_ball.style.borderColor = "maroon";
|
chat_live_ball.style.borderColor = "maroon";
|
||||||
chat_live_status.innerText = "Disconnected from chat";
|
chat_live_status.innerText = "Disconnected from chat";
|
||||||
setTimeout(connect_websocket, websocket_backoff);
|
if (!ws.successor) {
|
||||||
websocket_backoff = Math.min(32000, websocket_backoff * 2);
|
ws.successor = true;
|
||||||
console.log("websocket closed", event);
|
setTimeout(connect_websocket, websocket_backoff);
|
||||||
|
websocket_backoff = Math.min(32000, websocket_backoff * 2);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ws.addEventListener("error", (event) => {
|
ws.addEventListener("error", (event) => {
|
||||||
|
console.log("websocket error", event);
|
||||||
chat_form_submit.disabled = true;
|
chat_form_submit.disabled = true;
|
||||||
chat_live_ball.style.borderColor = "maroon";
|
chat_live_ball.style.borderColor = "maroon";
|
||||||
chat_live_status.innerText = "Error connecting to chat";
|
chat_live_status.innerText = "Error connecting to chat";
|
||||||
console.log("websocket error", event);
|
|
||||||
});
|
});
|
||||||
ws.addEventListener("message", on_websocket_message);
|
ws.addEventListener("message", on_websocket_message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ async def websocket_outbound(queue):
|
||||||
payload = await queue.get()
|
payload = await queue.get()
|
||||||
await websocket.send_json(payload)
|
await websocket.send_json(payload)
|
||||||
|
|
||||||
async def websocket_inbound(secret, connected_websockets, chat, token):
|
async def websocket_inbound(connected_websockets, token, secret, chat):
|
||||||
while True:
|
while True:
|
||||||
receipt = await websocket.receive_json()
|
receipt = await websocket.receive_json()
|
||||||
receipt, error = parse(chat.keys(), secret, receipt)
|
receipt, error = parse(chat.keys(), secret, receipt)
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
secret = "test"
|
secret_key = "test"
|
||||||
auth_username = "broadcaster"
|
auth_username = "broadcaster"
|
||||||
|
segments_dir = "stream/"
|
||||||
|
|
読み込み中…
新しいイシューから参照