Compare commits

...

7 Commits

Author SHA1 Message Date
Fierelier 2efbaad7b9 Update README.md 2023-11-08 12:14:04 +01:00
Fierelier 5db6970447 Remake broadcast-screen 2023-11-08 11:44:33 +01:00
Fierelier 1bc3b15b60 Change how the client works 2023-11-08 11:43:24 +01:00
Fierelier 45ed3cbd03 Convert all - in variable names to _ 2023-11-08 11:42:50 +01:00
Fierelier 4eacd8c13b Fix token login 2023-11-08 11:42:23 +01:00
Fierelier 299016c965 Do not use exception hook 2023-11-08 11:35:41 +01:00
Fierelier c617b25c59 Add missing setting 2023-11-08 08:59:28 +01:00
6 changed files with 137 additions and 145 deletions

View File

@ -3,39 +3,61 @@ A TCP multi-broadcast media streaming server/client. [Check out the past source]
# Client
## Token
`fstream.py <ip:port> token`
Gives you a token you can broadcast with. By default, tokens expire after 2 minutes. They can only be used once, and only one token can exist per user.
This is useful for getting a one-time access via HTTPS, and then using it to stream via HTTP, avoiding the CPU overhead of HTTPS and the security risk of HTTP. Think of it like a handshake.
### Server environment variables (fserv_arg_*)
- **`user`**: Your user's name.
- **`user_password`**: Your user's password.
## Broadcast
`fstream.py <ip:port> broadcast,[key=value],[key=value],...`
`fstream.py <ip:port> broadcast`
Accepts data from stdin, and sends it to the specified server.
### Arguments
### Server environment variables (fserv_arg_*)
- **`user`**: Your user's name.
- **`user-password`**: Your user's password.
- **`user_password`**: Your user's password. Optional, if you use `token`.
- **`token`**: Your temporary token. Optional, if you use `user_password`.
- **`channel`**: The channel you wanna stream to. Can be any name. Defaults to default.
- **`channel-password`**: The channel's password. Can be any password. Defaults to no password.
- **`bufsize`**: The size of chunks. Defaults to 0 (no set size, lowest delay).
- **`channel_password`**: The channel's password. Can be any password. Defaults to no password.
- **`bufsize`**: The size of chunks. If someone watches your stream, it will begin at the start of a chunk. Defaults to 0 (no set size, lowest delay).
All arguments are optional but for `user` and `user-password`.
All arguments are optional but for `user` and one of `user_password` or `token`.
### Example
`ffmpeg -f gdigrab -framerate 30 -i desktop -vf scale=-2:480 -c:v libx264 -pix_fmt yuv420p -maxrate 1M -f h264 - | fstream.py 127.0.0.1:61920 broadcast,user=fier,user-password=123,channel=exampleChannel,channel-password=456`
```bash
export fserv_arg_user="fier"
export fserv_arg_user_password="123"
export fserv_arg_user_channel="default"
export fserv_arg_channel_password="456"
ffmpeg -f gdigrab -framerate 30 -i desktop -vf scale=-2:480 -c:v libx264 -pix_fmt yuv420p -maxrate 1M -f h264 - | ./fstream.py 127.0.0.1:61920 broadcast
```
Broadcast Windows desktop as `fier` to `127.0.0.1:61920`, supplying `123` as the user password. `exampleChannel` is the channel, `456` is the channel's password. Pipe the output from ffmpeg.
Broadcast Windows desktop as `fier` to `127.0.0.1:61920`, supplying `123` as the user password. `default` is the channel, `456` is the channel's password. Pipe the output from ffmpeg.
## Watch
`fstream.py <ip:port> broadcast,[key=value],[key=value],...`
`fstream.py <ip:port> broadcast`
Accepts data from the server, and sends it to stdout.
### Arguments
### Server environment variables (fserv_arg_*)
- **`user`**: The user you wanna watch.
- **`channel`**: The user's channel you wanna watch.
- **`channel-password`**: The channel's password.
- **`channel_password`**: The channel's password.
All arguments are optional but for `user`.
### Example
`fstream.py 127.0.0.1:61920 watch,user=fier,channel=exampleChannel,channel-password=123 | ffplay -i -`
```bash
export fserv_arg_user="fier"
export fserv_arg_channel="default"
export fserv_arg_channel_password="456"
./fstream.py 127.0.0.1:61920 watch | ffplay -i -
```
Watch `fier`'s `exampleChannel` channel at `127.0.0.1:61920`, supplying `123` as the password, and pipe it into ffplay for playback.
Watch `fier`'s `default` channel at `127.0.0.1:61920`, supplying `456` as the password, and pipe it into ffplay for playback.
# Server
## Settings
@ -54,6 +76,6 @@ If you would like to implement your own authentication, make your own module to
Establish a TCP connection with the server, and send the payload. If the server likes your payload, it will stream data to you, or accept more of your data.
## The payload
First, send two new line characters (`\n\n`) (could also be HTTP headers, as they end in `\n\n`). Then, send the length of the payload as a 4-byte (32-bit) big endian unsigned integer, a null byte (hex:`00`) and a UTF-8 encoded string identifying the client's intentions follows, for example: `watch,user=fier,channel=exampleChannel,channel-password=123` or `broadcast,user=fier,user-password=123,channel=exampleChannel,channel-password=456`. The length includes only the string message, in bytes, it does not include the length itself, nor the null byte.
First, send two new line characters (`\n\n`) (could also be HTTP headers, as they end in `\n\n`). Then, send the length of the payload as a 4-byte (32-bit) big endian unsigned integer, a null byte (hex:`00`) and a UTF-8 encoded string identifying the client's intentions follows, for example: `watch,user=fier,channel=default,channel_password=456` or `broadcast,user=fier,user_password=123,channel=default,channel_password=456`. The length includes only the string message, in bytes, it does not include the length itself, nor the null byte.
If you are a watcher, you will now be blasted with data. If you are a broadcaster, you can now blast data.

View File

@ -1,52 +1,35 @@
#!/usr/bin/env bash
cd "$(dirname "$(realpath "$BASH_SOURCE")")"
function getValue() {
var="$1"
if ! [ "${!var}" == "" ]; then
return
fi
if [ "$3" == "fail" ]; then
echo "ERROR: $1 is not defined."
exit 1
fi
declare -g "$1"="$2"
sys_python="python3"
function setUser() {
export fstream_arg_user="user"
export fstream_arg_user_password="123"
}
function setChannel() {
export fstream_arg_channel="default"
export fstream_arg_channel_password="456"
}
# USER SETTINGS
getValue fstream_profile "profile_broadcast-screen.sh"
source "$fstream_profile"
getValue fstream_resolution "480"
getValue fstream_framerate "15"
getValue fstream_bitrate "1M"
getValue fstream_264profile "ultrafast"
export fstream_arg_bufsize="4096"
aes_password="789"
getValue fstream_ip "fier.me:61920" fail
getValue fstream_user "user" fail
getValue fstream_password "123" fail
getValue fstream_channel "default"
getValue fstream_channelpass ""
getValue fstream_python "python3"
enc_resolution="480"
enc_framerate="15"
enc_bitrate="1M"
enc_preset="ultrafast"
getValue fstream_aespass ""
getValue fstream_aesbuffer "4096"
getValue fstream_buffer "$((fstream_aesbuffer + 16))"
getValue fstream_ssl "0"
getValue fstream_ssl_ignoreCert "0"
fstream_encodercmd=(
enc_cmd=(
ffmpeg
# INPUT
-strict experimental -avioflags direct -thread_queue_size 1 -hwaccel auto -probesize 32 -fflags nobuffer -flags low_delay -flags2 fast # delay hack
-f x11grab -framerate "$fstream_framerate" -i "$DISPLAY" # linux
#-f gdigrab -framerate "$fstream_framerate" -i desktop # windows
-vf scale=-2:$fstream_resolution
-f x11grab -framerate "$enc_framerate" -i "$DISPLAY" # linux
#-f gdigrab -framerate "$enc_framerate" -i desktop # windows
-vf "scale=-2:$enc_resolution"
# ENCODING
-max_probe_packets 0 -max_delay 0 -flags2 fast # delay hack
-c:v libx264 -pix_fmt yuv420p -preset $fstream_264profile -tune zerolatency -x264-params "nal-hrd=cbr" -b:v $fstream_bitrate -minrate $fstream_bitrate -maxrate $fstream_bitrate -bufsize $fstream_bitrate*2
-c:v libx264 -pix_fmt yuv420p -preset "$enc_preset" -tune zerolatency -x264-params "nal-hrd=cbr" -b:v "$enc_bitrate" -minrate "$enc_bitrate" -maxrate "$enc_bitrate" -bufsize "${enc_bitrate}*2"
-x264opts intra-refresh=1 # delay hack
# OUTPUT
@ -56,20 +39,32 @@ ffmpeg
-
)
fstream_clientarg="broadcast,user=$fstream_user,user-password=$fstream_password,channel=$fstream_channel,channel-password=$fstream_channelpass"
if [ "$fstream_buffer" -gt "0" ]; then
fstream_clientarg="$fstream_clientarg,bufsize=$fstream_buffer"
if [ "$fstream_arg_bufsize" == "" ] || [ "$fstream_arg_bufsize" == "0" ]; then
unset fstream_arg_bufsize
fi
fstream_clientcmd=(
"$fstream_python" fstream.py
"$fstream_ip"
"$fstream_clientarg"
)
if [ "$fstream_aespass" == "" ]; then
"${fstream_encodercmd[@]}" | "${fstream_clientcmd[@]}"
else
"${fstream_encodercmd[@]}" | fstream_aespass="$fstream_aespass" fstream_aesbuffer="$fstream_aesbuffer" "$fstream_python" fstream-util-pipe_to_aes.py | "${fstream_clientcmd[@]}"
fi
export fstream_arg_token=""
setChannel
while :; do
# Get token
export fstream_ssl="1"
export fstream_ssl_ignoreCert="1"
setUser
unset fstream_arg_token
unset fstream_arg_channel
unset fstream_arg_channel_password
export fstream_arg_token="$("$sys_python" fstream.py "fier.me:61921" token)"
unset fstream_arg_user_password
# Broadcast
unset fstream_ssl
unset fstream_ssl_ignoreCert
setChannel
if ! [ "$fstream_aespass" == "" ]; then
"${enc_cmd[@]}" | fstream_aespass="$aes_password" fstream_aesbuffer="$(($fstream_arg_bufsize - 16))" "$sys_python" fstream-util-pipe_to_aes.py | "$sys_python" fstream.py "fier.me:61920" broadcast
else
"${enc_cmd[@]}" | "$sys_python" fstream.py "fier.me:61920" broadcast
fi
sleep 1
done

View File

@ -1,12 +1,5 @@
#!/usr/bin/env python3
import sys
oldexcepthook = sys.excepthook
def newexcepthook(type,value,traceback):
oldexcepthook(type,value,traceback)
#input("Press ENTER to quit.")
sys.excepthook = newexcepthook
import os
p = os.path.join
pUp = os.path.dirname
@ -23,17 +16,20 @@ import socket
import time
def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)
def defGet(tbl,key,defv):
if not key in tbl: return defv
return tbl[key]
bufferSize = 4096 # max buffer size in bytes for receiving data, lower values shouldn't reduce the delay
bufferSizeStdin = 128 # min buffer size for buffer, lower values DO reduce delay but raise CPU usage
timeout = 15 # timeout in seconds
connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
unbufferedStdout = os.fdopen(sys.stdout.fileno(),"wb",0) # Make unbuffered stdout
bufferSize = int(defGet(os.environ,"fstream_net_buffer","4096")) # max buffer size in bytes for receiving data, lower values shouldn't reduce the delay
bufferSizeStdin = int(defGet(os.environ,"fstream_stdin_buffer","128")) # min buffer size for buffer, lower values DO reduce delay but raise CPU usage
timeout = float(defGet(os.environ,"fstream_net_timeout","15")) # timeout in seconds
useSSL = False
sslIgnoreCert = False
if "fstream_ssl" in os.environ and os.environ["fstream_ssl"] == "1": useSSL = True
if "fstream_ssl_ignoreCert" in os.environ and os.environ["fstream_ssl_ignoreCert"] == "1": sslIgnoreCert = True
if defGet(os.environ,"fstream_ssl","0") == "1": useSSL = True
if defGet(os.environ,"fstream_ssl_ignoreCert","0") == "1": sslIgnoreCert = True
connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
unbufferedStdout = os.fdopen(sys.stdout.fileno(),"wb",0) # Make unbuffered stdout
def listToCommand(lst):
cmd = ""
@ -44,32 +40,6 @@ def listToCommand(lst):
return cmd[:-1]
def commandToList(cmd):
args = []
cArg = ""
escape = False
for letter in cmd:
if escape == True:
cArg += letter
escape = False
continue
if letter == "\\":
escape = True
continue
if letter == ",":
if cArg == "": continue
args.append(cArg)
cArg = ""
continue
cArg += letter
args.append(cArg)
return args
def recv(conn,l):
start = time.process_time()
timeo = conn.gettimeout()
@ -107,6 +77,12 @@ def main():
serverAddr = stringToAddressTuple(sys.argv[1])
if useSSL: import ssl
serverEnv = [sys.argv[2]]
for var in os.environ:
if not var.startswith("fstream_arg_"): continue
serverEnv.append(var.replace("fstream_arg_","",1)+ "=" +os.environ[var])
serverEnv = listToCommand(serverEnv)
eprint("Connecting to server...")
connection.settimeout(timeout)
connection.connect(serverAddr)
@ -129,16 +105,30 @@ def main():
eprint("Sending payload...")
connection.sendall("\n\n".encode("ascii"))
sendResponse(connection,sys.argv[2].encode("utf-8"))
sendResponse(connection,serverEnv.encode("utf-8"))
cmd = commandToList(sys.argv[2])
args = {}
for arg in cmd[1:]:
argSplit = arg.split("=",1)
args[argSplit[0]] = argSplit[1]
cmd = cmd[0]
if sys.argv[2] == "token":
try:
token = b""
eprint("Receiving token...")
while True:
data = connection.recv(bufferSize)
if data != b"":
token += data
continue
if token == b"":
eprint("Connection closed: failure")
os.exit(1)
else:
eprint("Connection closed: success")
unbufferedStdout.write(token)
return
except:
connection.close()
raise
if cmd == "watch":
if sys.argv[2] == "watch":
try:
eprint("Receiving data...")
while True:
@ -151,11 +141,13 @@ def main():
connection.close()
raise
if cmd == "broadcast":
if sys.argv[2] == "broadcast":
try:
eprint("Sending data...")
while True:
data = sys.stdin.buffer.read(bufferSizeStdin)
if data == b"":
return
connection.sendall(data)
except:
connection.close()

View File

@ -1,19 +0,0 @@
fstream_python="python3"
fstream_ip="fier.me:61920"
#fstream_ssl="0"
#fstream_ssl_ignoreCert="0"
fstream_user="user"
fstream_password="123"
fstream_channel="default"
fstream_channelpass=""
fstream_resolution="480"
fstream_framerate="15"
fstream_bitrate="1M"
fstream_profile="ultrafast"
fstream_aespass=""
fstream_aesbuffer="4096"
fstream_buffer="$((fstream_aesbuffer + 16))"

View File

@ -2,8 +2,8 @@ global select
import select
global time
import time
global binascii
import binascii
global codecs
import codecs
global clientLoopIn
def clientLoopIn(self):
@ -33,7 +33,7 @@ def clientLoopIn(self):
args[argSplit[0]] = argSplit[1]
if not "channel" in args: args["channel"] = "default"
if not "channel-password" in args: args["channel-password"] = ""
if not "channel_password" in args: args["channel_password"] = ""
with clientDataLock:
setClientData(self.cID,"type",cmd[0])
@ -51,13 +51,12 @@ def clientLoopIn(self):
q = queue.Queue()
setClientData(self.cID,"queue",q)
setClientData(self.cID,"active",False)
token = os.urandom(tokenLength)
token = codecs.encode(os.urandom(tokenLength),"hex")
setClientData(self.cID,"token",token)
if cmd[0] == "token":
if not authenticate(args["user"],args["user-password"]): return
if not authenticate(args["user"],args["user_password"]): return
with clientDataLock:
setClientData(self.cID,"active",True)
with clientsLock:
for client in clients:
if getClientData(client,"type") != "token": continue
@ -65,9 +64,10 @@ def clientLoopIn(self):
if getClientData(client,"active") != True: continue
setClientData(client,"active",False)
getClientData(client,"queue").put(None)
setClientData(self.cID,"active",True)
ttimeout = time.monotonic()
self.connection.sendall(binascii.hexlify(token))
self.connection.sendall(token)
self.connection.close()
ttimeout = tokenTimeout - (time.monotonic() - ttimeout)
if ttimeout <= 0: return
@ -79,17 +79,17 @@ def clientLoopIn(self):
if cmd[0] == "broadcast":
if not "token" in args:
if not authenticate(args["user"],args["user-password"]): return
if not authenticate(args["user"],args["user_password"]): return
else:
tokenAuthed = False
with clientDataLock:
args["token"] = bytes.fromhex(args["token"])
args["token"] = args["token"].encode("ascii")
with clientsLock:
for client in clients:
if getClientData(client,"type") != "token": continue
if getClientData(client,"args")["user"] != args["user"]: continue
if getClientData(client,"active") != True: continue
if getClientData(client,"token") != token: return
if getClientData(client,"token") != args["token"]: return
tokenAuthed = True
break
if not tokenAuthed: return
@ -124,7 +124,7 @@ def clientLoopIn(self):
setClientData(self.cID,"bufferPacket",packet)
with clientsLock:
for cID in clients:
if getClientData(cID,"type") == "watch" and getClientData(cID,"args")["user"] == args["user"] and getClientData(cID,"args")["channel"] == args["channel"] and getClientData(cID,"args")["channel-password"] == args["channel-password"]:
if getClientData(cID,"type") == "watch" and getClientData(cID,"args")["user"] == args["user"] and getClientData(cID,"args")["channel"] == args["channel"] and getClientData(cID,"args")["channel_password"] == args["channel_password"]:
getClientData(cID,"queue").put("")
if cmd[0] == "watch":

View File

@ -4,6 +4,8 @@ global bufferCost
bufferCost = 1024 # Virtually add extra cost to each buffer piece to prevent clients from overloading the server by sending super small pieces.
global maxBuffer
maxBuffer = 20*1024*1024 # The maximum buffer size of a stream in bytes. Old buffers are discarded, clients that depend on them get disconnected.
global httpHeaderMaxSize
httpHeaderMaxSize = 16192 # Maximum size for faux incoming HTTP header
global tokenTimeout
tokenTimeout = 120.0 # How long it takes, in seconds, for a login token to time out.
global tokenLength