Compare commits
7 Commits
794c265c1d
...
2efbaad7b9
Author | SHA1 | Date |
---|---|---|
Fierelier | 2efbaad7b9 | |
Fierelier | 5db6970447 | |
Fierelier | 1bc3b15b60 | |
Fierelier | 45ed3cbd03 | |
Fierelier | 4eacd8c13b | |
Fierelier | 299016c965 | |
Fierelier | c617b25c59 |
50
README.md
50
README.md
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))"
|
|
@ -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":
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue