song transitioning PoC
This commit is contained in:
parent
52622b22e7
commit
cccc3f4dc5
@ -12,6 +12,8 @@ import os
|
|||||||
import pip
|
import pip
|
||||||
import signal
|
import signal
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
from threading import Thread, main_thread
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
def updateYoutubeDL():
|
def updateYoutubeDL():
|
||||||
pip.main(['install', '--target=' + dirpath, '--upgrade', 'youtube_dl'])
|
pip.main(['install', '--target=' + dirpath, '--upgrade', 'youtube_dl'])
|
||||||
@ -23,40 +25,55 @@ dirpath = tempfile.mkdtemp()
|
|||||||
sys.path.append(dirpath)
|
sys.path.append(dirpath)
|
||||||
updateYoutubeDL()
|
updateYoutubeDL()
|
||||||
|
|
||||||
def executeYoutubeDL(url, cb):
|
class Downloader(Thread):
|
||||||
env = dict(os.environ)
|
|
||||||
env["PYTHONPATH"] = dirpath
|
|
||||||
cmd = [
|
|
||||||
sys.executable,
|
|
||||||
dirpath + "/bin/youtube-dl",
|
|
||||||
"-o", "-",
|
|
||||||
"-f", "bestaudio/best",
|
|
||||||
# "--audio-format", "mp3", "-x", # cannot do because it requires a tmp file to re-encode
|
|
||||||
"--prefer-ffmpeg",
|
|
||||||
"--no-mark-watched",
|
|
||||||
"--geo-bypass",
|
|
||||||
"--no-playlist",
|
|
||||||
"--retries", "100",
|
|
||||||
"--no-call-home",
|
|
||||||
url
|
|
||||||
]
|
|
||||||
popen = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid)
|
|
||||||
|
|
||||||
# monitor the stdout and send to callback, if result from callback function is true,
|
def __init__(self, url, cb):
|
||||||
# then kill the download process
|
Thread.__init__(self)
|
||||||
BUFFER_SIZE = 8096
|
|
||||||
logger.add(popen.stderr, "youtube-dl.log")
|
|
||||||
for chunk in iter(lambda: popen.stdout.read(BUFFER_SIZE), b''):
|
|
||||||
if cb(chunk):
|
|
||||||
os.killpg(os.getpgid(popen.pid), signal.SIGTERM)
|
|
||||||
break
|
|
||||||
popen.stdout.close()
|
|
||||||
popen.wait()
|
|
||||||
|
|
||||||
def download(url, cb):
|
# update youtube-dl
|
||||||
# update youtube-dl
|
# TODO: do this only every once in a while
|
||||||
# TODO: do this only every once in a while
|
# updateYoutubeDL()
|
||||||
# updateYoutubeDL()
|
|
||||||
|
|
||||||
# start downloader so that it's stdout (with fragments) may be captured
|
self.cb = cb
|
||||||
executeYoutubeDL(url, cb)
|
self.exit = False
|
||||||
|
|
||||||
|
env = dict(os.environ)
|
||||||
|
env["PYTHONPATH"] = dirpath
|
||||||
|
cmd = [
|
||||||
|
sys.executable,
|
||||||
|
dirpath + "/bin/youtube-dl",
|
||||||
|
"-o", "-",
|
||||||
|
"-f", "bestaudio/best",
|
||||||
|
# "--audio-format", "mp3", "-x", # cannot do because it requires a tmp file to re-encode
|
||||||
|
"--prefer-ffmpeg",
|
||||||
|
"--no-mark-watched",
|
||||||
|
"--geo-bypass",
|
||||||
|
"--no-playlist",
|
||||||
|
"--retries", "100",
|
||||||
|
"--no-call-home",
|
||||||
|
url
|
||||||
|
]
|
||||||
|
self.popen = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid)
|
||||||
|
logger.add(self.popen.stderr, "youtube-dl.log")
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def isAlive(self):
|
||||||
|
return self.popen.poll() is None
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self.isAlive():
|
||||||
|
os.killpg(os.getpgid(self.popen.pid), signal.SIGTERM)
|
||||||
|
self.popen.stdout.close()
|
||||||
|
self.popen.wait()
|
||||||
|
self.exit = True
|
||||||
|
|
||||||
|
# checks to see if the current download has finished
|
||||||
|
# if yes, cleans up and fires callback
|
||||||
|
def run(self):
|
||||||
|
while main_thread().is_alive() and not self.exit:
|
||||||
|
if not self.isAlive():
|
||||||
|
self.cb()
|
||||||
|
sleep(0.1)
|
||||||
|
|
||||||
|
def getStream(self):
|
||||||
|
return self.popen.stdout
|
46
logger.py
46
logger.py
@ -1,47 +1,47 @@
|
|||||||
import sys
|
import sys
|
||||||
import fcntl
|
|
||||||
import os
|
|
||||||
from subprocess import PIPE, Popen
|
from subprocess import PIPE, Popen
|
||||||
from threading import Thread, main_thread
|
from threading import Thread, main_thread, Lock
|
||||||
from queue import Queue, Empty
|
from queue import Queue, Empty
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from util import non_block_read
|
||||||
|
|
||||||
READ_SIZE = 8096
|
READ_SIZE = 8096
|
||||||
LOG_DIR = "logs/"
|
LOG_DIR = "logs/"
|
||||||
|
|
||||||
def non_block_read(output):
|
|
||||||
fd = output.fileno()
|
|
||||||
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
||||||
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
|
||||||
try:
|
|
||||||
return output.read()
|
|
||||||
except:
|
|
||||||
return b''
|
|
||||||
|
|
||||||
class Logger(Thread):
|
class Logger(Thread):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
self.streams = dict()
|
self.streams = dict()
|
||||||
self.files = dict()
|
self.files = dict()
|
||||||
|
self.mutex = Lock()
|
||||||
Path(LOG_DIR).mkdir(exist_ok=True)
|
Path(LOG_DIR).mkdir(exist_ok=True)
|
||||||
|
|
||||||
def add(self, stream, filename):
|
def add(self, stream, filename):
|
||||||
self.files[filename] = open(LOG_DIR + filename, "wb")
|
self.mutex.acquire()
|
||||||
self.streams[filename] = stream
|
try:
|
||||||
|
if not filename in self.files.keys():
|
||||||
|
self.files[filename] = open(LOG_DIR + filename, "ab")
|
||||||
|
self.streams[filename] = stream
|
||||||
|
finally:
|
||||||
|
self.mutex.release()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while main_thread().is_alive():
|
while main_thread().is_alive():
|
||||||
for filename, stream in self.streams.items():
|
self.mutex.acquire()
|
||||||
f = self.files[filename]
|
try:
|
||||||
while True:
|
for filename, stream in self.streams.items():
|
||||||
output = non_block_read(stream)
|
f = self.files[filename]
|
||||||
if output == None or output == b'':
|
while True:
|
||||||
break
|
output = non_block_read(stream)
|
||||||
print(output.decode('ascii'))
|
if output == None or output == b'':
|
||||||
f.write(output)
|
break
|
||||||
f.flush()
|
print(output.decode('ascii'))
|
||||||
|
f.write(output)
|
||||||
|
f.flush()
|
||||||
|
finally:
|
||||||
|
self.mutex.release()
|
||||||
sleep(0.1)
|
sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
45
radio.py
45
radio.py
@ -1,26 +1,35 @@
|
|||||||
import downloader
|
import downloader
|
||||||
import ffmpeg
|
import uploader
|
||||||
import sys
|
import transcoder
|
||||||
from logger import logger
|
from time import sleep
|
||||||
|
|
||||||
def run():
|
def cb():
|
||||||
process = ( ffmpeg
|
global d
|
||||||
.input('pipe:', re=None)
|
global t
|
||||||
.output("icecast://source:hackme@localhost:8000/stream.mp3", format='mp3', content_type="audio/mpeg")
|
print("----------------FINISHED-----------")
|
||||||
.run_async(pipe_stdin=True, pipe_stdout=True, pipe_stderr=True)
|
t.listener.blockUntilEmpty()
|
||||||
)
|
u.listener.blockUntilEmpty()
|
||||||
|
d.stop()
|
||||||
|
t.stop()
|
||||||
|
d = downloader.Downloader('https://www.youtube.com/watch?v=BQQ3qZ9FC70', cb)
|
||||||
|
t = transcoder.Transcoder(d)
|
||||||
|
u.setUpstream(t)
|
||||||
|
|
||||||
logger.add(process.stdout, "ffmpeg.out.log")
|
u = uploader.Uploader()
|
||||||
logger.add(process.stderr, "ffmpeg.err.log")
|
d = downloader.Downloader('https://www.youtube.com/watch?v=kgBcg4uBd9Q', cb)
|
||||||
|
t = transcoder.Transcoder(d)
|
||||||
|
u.setUpstream(t)
|
||||||
|
|
||||||
def cb(chunk):
|
#def run():
|
||||||
process.stdin.write(chunk)
|
# up = uploader.Uploader()
|
||||||
return False
|
# t = transcoder.Transcoder(up)
|
||||||
|
# downloader.download('https://www.youtube.com/watch?v=BaW_jenozKc', t.callback)
|
||||||
downloader.download('https://www.youtube.com/watch?v=BaW_jenozKc', cb)
|
# t = transcoder.Transcoder(up)
|
||||||
downloader.download('https://www.youtube.com/watch?v=EbnH3VHzhu8', cb)
|
# downloader.download('https://www.youtube.com/watch?v=EbnH3VHzhu8', t.callback)
|
||||||
# downloader.download('https://www.youtube.com/watch?v=kgBcg4uBd9Q', cb)
|
# downloader.download('https://www.youtube.com/watch?v=kgBcg4uBd9Q', cb)
|
||||||
# downloader.download('https://www.youtube.com/watch?v=EbnH3VHzhu8', cb)
|
# downloader.download('https://www.youtube.com/watch?v=EbnH3VHzhu8', cb)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run()
|
#run()
|
||||||
|
while True:
|
||||||
|
sleep(1)
|
2
setup.py
2
setup.py
@ -5,7 +5,7 @@ requires = ["pip","ffmpeg-python"]
|
|||||||
setup(
|
setup(
|
||||||
name='radio',
|
name='radio',
|
||||||
version='0.1',
|
version='0.1',
|
||||||
py_modules=['radio','downloader','logger'],
|
py_modules=['radio','downloader','uploader','logger','util'],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': ['radio = radio:run']
|
'console_scripts': ['radio = radio:run']
|
||||||
},
|
},
|
||||||
|
49
stream_listener.py
Normal file
49
stream_listener.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from threading import Thread, main_thread
|
||||||
|
from util import non_block_read, non_block_peek
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
#
|
||||||
|
# In a new thread, listens for new data on the stream
|
||||||
|
# and passes it to the listener when available
|
||||||
|
#
|
||||||
|
# Upstreams implement getStream()
|
||||||
|
# Downstreams implement write() for bytes to be given to
|
||||||
|
#
|
||||||
|
class StreamListener(Thread):
|
||||||
|
|
||||||
|
def __init__(self, upstream, downstream):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.setUpstream(upstream)
|
||||||
|
self.setDownstream(downstream)
|
||||||
|
self.quit = False
|
||||||
|
|
||||||
|
def setUpstream(self, upstream):
|
||||||
|
if not upstream is None:
|
||||||
|
self.stream = upstream.getStream()
|
||||||
|
else:
|
||||||
|
self.stream = None
|
||||||
|
|
||||||
|
def setDownstream(self, downstream):
|
||||||
|
self.listener = downstream
|
||||||
|
|
||||||
|
# blocks until there is nothing left to read from upstream
|
||||||
|
def blockUntilEmpty(self):
|
||||||
|
if not self.stream is None:
|
||||||
|
while True:
|
||||||
|
output = non_block_peek(self.stream)
|
||||||
|
if output == None or output == b'':
|
||||||
|
break
|
||||||
|
sleep(0.1)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.quit = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while main_thread().is_alive() and not self.quit:
|
||||||
|
while True:
|
||||||
|
if not self.stream is None:
|
||||||
|
output = non_block_read(self.stream)
|
||||||
|
if output == None or output == b'':
|
||||||
|
break
|
||||||
|
self.listener.write(output)
|
||||||
|
sleep(0.1)
|
29
transcoder.py
Normal file
29
transcoder.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import ffmpeg
|
||||||
|
from logger import logger
|
||||||
|
from stream_listener import StreamListener
|
||||||
|
|
||||||
|
# converts the stream to mp3 before sending to the long-lasting mp3 connection
|
||||||
|
class Transcoder(object):
|
||||||
|
|
||||||
|
def __init__(self, upstream):
|
||||||
|
self.process = ( ffmpeg
|
||||||
|
.input('pipe:')
|
||||||
|
.output('pipe:', format='mp3')
|
||||||
|
.run_async(pipe_stdin=True, pipe_stdout=True, pipe_stderr=True)
|
||||||
|
)
|
||||||
|
logger.add(self.process.stderr, "transcoder.log")
|
||||||
|
self.listener = StreamListener(upstream, self)
|
||||||
|
self.listener.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.listener.stop()
|
||||||
|
self.process.stdin.close()
|
||||||
|
self.process.stdout.close()
|
||||||
|
self.process.stderr.close()
|
||||||
|
self.process.wait()
|
||||||
|
|
||||||
|
def getStream(self):
|
||||||
|
return self.process.stdout
|
||||||
|
|
||||||
|
def write(self, chunk):
|
||||||
|
self.process.stdin.write(chunk)
|
42
uploader.py
Normal file
42
uploader.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import ffmpeg
|
||||||
|
from logger import logger
|
||||||
|
import sys
|
||||||
|
from stream_listener import StreamListener
|
||||||
|
|
||||||
|
class Uploader(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.process = ( ffmpeg
|
||||||
|
.input('pipe:', re=None)
|
||||||
|
.output("icecast://source:hackme@localhost:8000/stream.mp3", format='mp3', content_type="audio/mpeg")
|
||||||
|
.run_async(pipe_stdin=True, pipe_stderr=True)
|
||||||
|
)
|
||||||
|
logger.add(self.process.stderr, "uploader.log")
|
||||||
|
self.listener = StreamListener(None, self)
|
||||||
|
self.listener.start()
|
||||||
|
|
||||||
|
def reconnect(self):
|
||||||
|
self.process.stdin.close()
|
||||||
|
self.process.wait()
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def setUpstream(self, upstream):
|
||||||
|
self.listener.setUpstream(upstream)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.listener.stop()
|
||||||
|
self.process.stdin.close()
|
||||||
|
self.process.stderr.close()
|
||||||
|
self.process.wait()
|
||||||
|
|
||||||
|
def write(self, chunk):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self.process.stdin.write(chunk)
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
print("Unexpected error:", sys.exc_info())
|
||||||
|
self.reconnect()
|
24
util.py
Normal file
24
util.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import fcntl
|
||||||
|
import os
|
||||||
|
|
||||||
|
def non_block_read(output):
|
||||||
|
if output.closed:
|
||||||
|
return b''
|
||||||
|
fd = output.fileno()
|
||||||
|
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||||
|
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
||||||
|
try:
|
||||||
|
return output.read()
|
||||||
|
except:
|
||||||
|
return b''
|
||||||
|
|
||||||
|
def non_block_peek(output):
|
||||||
|
if output.closed:
|
||||||
|
return b''
|
||||||
|
fd = output.fileno()
|
||||||
|
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||||
|
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
||||||
|
try:
|
||||||
|
return output.peek()
|
||||||
|
except:
|
||||||
|
return b''
|
Loading…
x
Reference in New Issue
Block a user