forked from dancardy/stream2chromecast
-
Notifications
You must be signed in to change notification settings - Fork 0
/
stream2chromecast.py
executable file
·798 lines (527 loc) · 23.9 KB
/
stream2chromecast.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
#!/usr/bin/env python
"""
stream2chromecast.py: Chromecast media streamer for Linux
author: Pat Carter - https://github.com/Pat-Carter/stream2chromecast
version: 0.6.3
"""
# Copyright (C) 2014-2016 Pat Carter
#
# This file is part of Stream2chromecast.
#
# Stream2chromecast is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Stream2chromecast is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Stream2chromecast. If not, see <http://www.gnu.org/licenses/>.
VERSION = "0.6.3"
import sys, os, errno
import signal
from cc_media_controller import CCMediaController
import cc_device_finder
import time
import BaseHTTPServer
import urllib
import mimetypes
from threading import Thread
import subprocess
import httplib
import urlparse
import socket
import tempfile
script_name = (sys.argv[0].split(os.sep))[-1]
USAGETEXT = """
Usage
Play a file:-
%s <file>
Pause the current file:-
%s -pause
Continue (un-pause) the current file:-
%s -continue
Stop the current file playing:-
%s -stop
Set the volume to a value between 0 & 1.0 (e.g. 0.5 = half volume):-
%s -setvol <volume>
Adjust the volume up or down by 0.1:-
%s -volup
%s -voldown
Mute the volume:-
%s -mute
Play an unsupported media type (e.g. an mpg file) using ffmpeg or avconv as a realtime transcoder (requires ffmpeg or avconv to be installed):-
%s -transcode <file>
Play remote file using a URL (e.g. a web video):
%s -playurl <URL>
Display Chromecast status:-
%s -status
Search for all Chromecast devices on the network:-
%s -devicelist
Additional option to specify an Chromecast device by name (or ip address) explicitly:
e.g. to play a file on a specific device
%s -devicename <chromecast device name> <file>
Additional option to specify the preferred transcoder tool when both ffmpeg & avconv are available
e.g. to play and transcode a file using avconv
%s -transcoder avconv -transcode <file>
Additional option to specify the port from which the media is streamed. This can be useful in a firewalled environment.
e.g. to serve the media on port 8765
%s -port 8765 <file>
Additional option to specify subtitles. Only WebVTT format is supported.
e.g. to cast the subtitles on /path/to/subtitles.vtt
%s -subtitles /path/to/subtitles.vtt <file>
Additional option to specify the port from which the subtitles is streamed. This can be useful in a firewalled environment.
e.g. to serve the subtitles on port 8765
%s -subtitles_port 8765 <file>
Additional option to specify the subtitles language. The language format is defined by RFC 5646.
e.g. to serve the subtitles french subtitles
%s -subtitles_language fr <file>
Additional option to supply custom parameters to the transcoder (ffmpeg or avconv) output
e.g. to transcode the media with an output video bitrate of 1000k
%s -transcode -transcodeopts '-b:v 1000k' <file>
Additional option to supply custom parameters to the transcoder input
e.g. to transcode the media and seek to a position 15 minutes from the start of playback
%s -transcode -transcodeinputopts '-ss 00:15:00' <file>
Additional option to specify the buffer size of the data returned from the transcoder. Increasing this can help when on a slow network.
e.g. to specify a buffer size of 5 megabytes
%s -transcode -transcodebufsize 5242880 <file>
""" % ((script_name,) * 21)
PIDFILE = os.path.join(tempfile.gettempdir(), "stream2chromecast_%s.pid")
FFMPEG = 'ffmpeg %s -i "%s" -preset ultrafast -f mp4 -frag_duration 3000 -b:v 2000k -loglevel error %s -'
AVCONV = 'avconv %s -i "%s" -preset ultrafast -f mp4 -frag_duration 3000 -b:v 2000k -loglevel error %s -'
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
content_type = "video/mp4"
""" Handle HTTP requests for files which do not need transcoding """
def do_GET(self):
query = self.path.split("?",1)[-1]
filepath = urllib.unquote_plus(query)
self.suppress_socket_error_report = None
self.send_headers(filepath)
print "sending data"
try:
self.write_response(filepath)
except socket.error, e:
if isinstance(e.args, tuple):
if e[0] in (errno.EPIPE, errno.ECONNRESET):
print "disconnected"
self.suppress_socket_error_report = True
return
raise
def handle_one_request(self):
try:
return BaseHTTPServer.BaseHTTPRequestHandler.handle_one_request(self)
except socket.error:
if not self.suppress_socket_error_report:
raise
def finish(self):
try:
return BaseHTTPServer.BaseHTTPRequestHandler.finish(self)
except socket.error:
if not self.suppress_socket_error_report:
raise
def send_headers(self, filepath):
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.send_header("Content-type", self.content_type)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
def write_response(self, filepath):
with open(filepath, "rb") as f:
while True:
line = f.read(1024)
if len(line) == 0:
break
chunk_size = "%0.2X" % len(line)
self.wfile.write(chunk_size)
self.wfile.write("\r\n")
self.wfile.write(line)
self.wfile.write("\r\n")
self.wfile.write("0")
self.wfile.write("\r\n\r\n")
class TranscodingRequestHandler(RequestHandler):
""" Handle HTTP requests for files which require realtime transcoding with ffmpeg """
transcoder_command = FFMPEG
transcode_options = ""
transcode_input_options = ""
bufsize = 0
def write_response(self, filepath):
if self.bufsize != 0:
print "transcode buffer size:", self.bufsize
ffmpeg_command = self.transcoder_command % (self.transcode_input_options, filepath, self.transcode_options)
ffmpeg_process = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE, shell=True, bufsize=self.bufsize)
for line in ffmpeg_process.stdout:
chunk_size = "%0.2X" % len(line)
self.wfile.write(chunk_size)
self.wfile.write("\r\n")
self.wfile.write(line)
self.wfile.write("\r\n")
self.wfile.write("0")
self.wfile.write("\r\n\r\n")
class SubRequestHandler(RequestHandler):
""" Handle HTTP requests for subtitles files """
content_type = "text/vtt;charset=utf-8"
def get_transcoder_cmds(preferred_transcoder=None):
""" establish which transcoder utility to use depending on what is installed """
probe_cmd = None
transcoder_cmd = None
ffmpeg_installed = is_transcoder_installed("ffmpeg")
avconv_installed = is_transcoder_installed("avconv")
# if anything other than avconv is preferred, try to use ffmpeg otherwise use avconv
if preferred_transcoder != "avconv":
if ffmpeg_installed:
transcoder_cmd = "ffmpeg"
probe_cmd = "ffprobe"
elif avconv_installed:
print "unable to find ffmpeg - using avconv"
transcoder_cmd = "avconv"
probe_cmd = "avprobe"
# otherwise, avconv is preferred, so try to use avconv, followed by ffmpeg
else:
if avconv_installed:
transcoder_cmd = "avconv"
probe_cmd = "avprobe"
elif ffmpeg_installed:
print "unable to find avconv - using ffmpeg"
transcoder_cmd = "ffmpeg"
probe_cmd = "ffprobe"
return transcoder_cmd, probe_cmd
def is_transcoder_installed(transcoder_application):
""" check for an installation of either ffmpeg or avconv """
try:
subprocess.check_output([transcoder_application, "-version"])
return True
except OSError:
return False
def kill_old_pid(device_ip):
""" attempts to kill a previously running instance of this application casting to the specified device. """
pid_file = PIDFILE % device_ip
try:
with open(pid_file, "r") as pidfile:
pid = int(pidfile.read())
os.killpg(pid, signal.SIGTERM)
except:
pass
def save_pid(device_ip):
""" saves the process id of this application casting to the specified device in a pid file. """
pid_file = PIDFILE % device_ip
with open(pid_file, "w") as pidfile:
pidfile.write("%d" % os.getpid())
def get_mimetype(filename, ffprobe_cmd=None):
""" find the container format of the file """
# default value
mimetype = "video/mp4"
# guess based on filename extension
guess = mimetypes.guess_type(filename)[0]
if guess is not None:
if guess.lower().startswith("video/") or guess.lower().startswith("audio/"):
mimetype = guess
# use the OS file command...
try:
file_cmd = 'file --mime-type -b "%s"' % filename
file_mimetype = subprocess.check_output(file_cmd, shell=True).strip().lower()
if file_mimetype.startswith("video/") or file_mimetype.startswith("audio/"):
mimetype = file_mimetype
print "OS identifies the mimetype as :", mimetype
return mimetype
except:
pass
# use ffmpeg/avconv if installed
if ffprobe_cmd is None:
return mimetype
# ffmpeg/avconv is installed
has_video = False
has_audio = False
format_name = None
ffprobe_cmd = '%s -show_streams -show_format "%s"' % (ffprobe_cmd, filename)
ffmpeg_process = subprocess.Popen(ffprobe_cmd, stdout=subprocess.PIPE, shell=True)
for line in ffmpeg_process.stdout:
if line.startswith("codec_type=audio"):
has_audio = True
elif line.startswith("codec_type=video"):
has_video = True
elif line.startswith("format_name="):
name, value = line.split("=")
format_name = value.strip().lower().split(",")
# use the default if it isn't possible to identify the format type
if format_name is None:
return mimetype
if has_video:
mimetype = "video/"
else:
mimetype = "audio/"
if "mp4" in format_name:
mimetype += "mp4"
elif "webm" in format_name:
mimetype += "webm"
elif "ogg" in format_name:
mimetype += "ogg"
elif "mp3" in format_name:
mimetype = "audio/mpeg"
elif "wav" in format_name:
mimetype = "audio/wav"
else:
mimetype += "mp4"
return mimetype
def play(filename, transcode=False, transcoder=None, transcode_options=None, transcode_input_options=None,
transcode_bufsize=0, device_name=None, server_port=None,
subtitles=None, subtitles_port=None, subtitles_language=None):
""" play a local file or transcode from a file or URL and stream to the chromecast """
print_ident()
cast = CCMediaController(device_name=device_name)
kill_old_pid(cast.host)
save_pid(cast.host)
if os.path.isfile(filename):
filename = os.path.abspath(filename)
print "source is file: %s" % filename
else:
if transcode and (filename.lower().startswith("http://") or filename.lower().startswith("https://") or filename.lower().startswith("rtsp://")):
print "source is URL: %s" % filename
else:
sys.exit("media file %s not found" % filename)
transcoder_cmd, probe_cmd = get_transcoder_cmds(preferred_transcoder=transcoder)
status = cast.get_status()
webserver_ip = status['client'][0]
print "local ip address:", webserver_ip
req_handler = RequestHandler
if transcode:
if transcoder_cmd in ("ffmpeg", "avconv"):
req_handler = TranscodingRequestHandler
if transcoder_cmd == "ffmpeg":
req_handler.transcoder_command = FFMPEG
else:
req_handler.transcoder_command = AVCONV
if transcode_options is not None:
req_handler.transcode_options = transcode_options
if transcode_input_options is not None:
req_handler.transcode_input_options = transcode_input_options
req_handler.bufsize = transcode_bufsize
else:
print "No transcoder is installed. Attempting standard playback"
if req_handler == RequestHandler:
req_handler.content_type = get_mimetype(filename, probe_cmd)
# create a webserver to handle a single request for the media file on either a free port or on a specific port if passed in the port parameter
port = 0
if server_port is not None:
port = int(server_port)
server = BaseHTTPServer.HTTPServer((webserver_ip, port), req_handler)
thread = Thread(target=server.handle_request)
thread.start()
url = "http://%s:%s?%s" % (webserver_ip, str(server.server_port), urllib.quote_plus(filename, "/"))
print "URL & content-type: ", url, req_handler.content_type
# create another webserver to handle a request for the subtitles file, if specified in the subtitles parameter
sub = None
if subtitles:
if os.path.isfile(subtitles):
sub_port = 0
if subtitles_port is not None:
sub_port = int(subtitles_port)
sub_server = BaseHTTPServer.HTTPServer((webserver_ip, sub_port), SubRequestHandler)
thread2 = Thread(target=sub_server.handle_request)
thread2.start()
sub = "http://%s:%s?%s" % (webserver_ip, str(sub_server.server_port), urllib.quote_plus(subtitles, "/"))
print "sub URL: ", sub
else:
print "Subtitles file %s not found" % subtitles
load(cast, url, req_handler.content_type, sub, subtitles_language)
def load(cast, url, mimetype, sub=None, sub_language=None):
""" load a chromecast instance with a url and wait for idle state """
try:
print "loading media..."
cast.load(url, mimetype, sub, sub_language)
# wait for playback to complete before exiting
print "waiting for player to finish - press ctrl-c to stop..."
idle = False
while not idle:
time.sleep(1)
idle = cast.is_idle()
except KeyboardInterrupt:
print
print "stopping..."
cast.stop()
finally:
print "done"
def playurl(url, device_name=None):
""" play a remote HTTP resource on the chromecast """
print_ident()
def get_resp(url):
url_parsed = urlparse.urlparse(url)
scheme = url_parsed.scheme
host = url_parsed.netloc
path = url.split(host, 1)[-1]
conn = None
if scheme == "https":
conn = httplib.HTTPSConnection(host)
else:
conn = httplib.HTTPConnection(host)
conn.request("HEAD", path)
resp = conn.getresponse()
return resp
def get_full_url(url, location):
url_parsed = urlparse.urlparse(url)
scheme = url_parsed.scheme
host = url_parsed.netloc
if location.startswith("/") is False:
path = url.split(host, 1)[-1]
if path.endswith("/"):
path = path.rsplit("/", 2)[0]
else:
path = path.rsplit("/", 1)[0] + "/"
location = path + location
full_url = scheme + "://" + host + location
return full_url
resp = get_resp(url)
if resp.status != 200:
redirect_codes = [ 301, 302, 303, 307, 308 ]
if resp.status in redirect_codes:
redirects = 0
while resp.status in redirect_codes:
redirects += 1
if redirects > 9:
sys.exit("HTTP Error: Too many redirects")
headers = resp.getheaders()
for header in headers:
if len(header) > 1:
if header[0].lower() == "location":
redirect_location = header[1]
if redirect_location.startswith("http") is False:
redirect_location = get_full_url(url, redirect_location)
print "Redirecting to " + redirect_location
resp = get_resp(redirect_location)
if resp.status != 200:
sys.exit("HTTP error:" + str(resp.status) + " - " + resp.reason)
else:
sys.exit("HTTP error:" + str(resp.status) + " - " + resp.reason)
print "Found HTTP resource"
headers = resp.getheaders()
mimetype = None
for header in headers:
if len(header) > 1:
if header[0].lower() == "content-type":
mimetype = header[1]
if mimetype != None:
print "content-type:", mimetype
else:
mimetype = "video/mp4"
print "resource does not specify mimetype - using default:", mimetype
cast = CCMediaController(device_name=device_name)
load(cast, url, mimetype)
def pause(device_name=None):
""" pause playback """
CCMediaController(device_name=device_name).pause()
def unpause(device_name=None):
""" continue playback """
CCMediaController(device_name=device_name).play()
def stop(device_name=None):
""" stop playback and quit the media player app on the chromecast """
CCMediaController(device_name=device_name).stop()
def get_status(device_name=None):
""" print the status of the chromecast device """
print CCMediaController(device_name=device_name).get_status()
def volume_up(device_name=None):
""" raise the volume by 0.1 """
CCMediaController(device_name=device_name).set_volume_up()
def volume_down(device_name=None):
""" lower the volume by 0.1 """
CCMediaController(device_name=device_name).set_volume_down()
def set_volume(v, device_name=None):
""" set the volume to level between 0 and 1 """
CCMediaController(device_name=device_name).set_volume(v)
def list_devices():
print "Searching for devices, please wait..."
device_ips = cc_device_finder.search_network(device_limit=None, time_limit=10)
print "%d devices found" % len(device_ips)
for device_ip in device_ips:
print device_ip, ":", cc_device_finder.get_device_name(device_ip)
def print_ident():
""" display initial messages """
print
print "-----------------------------------------"
print
print "Stream2Chromecast version:%s" % VERSION
print
print "Copyright (C) 2014-2016 Pat Carter"
print "GNU General Public License v3.0"
print "https://www.gnu.org/licenses/gpl-3.0.html"
print
print "-----------------------------------------"
print
def validate_args(args):
""" validate that there are the correct number of arguments """
if len(args) < 1:
sys.exit(USAGETEXT)
if args[0] == "-setvol" and len(args) < 2:
sys.exit(USAGETEXT)
def get_named_arg_value(arg_name, args, integer=False):
""" get a argument value by name """
arg_val = None
if arg_name in args:
arg_pos = args.index(arg_name)
arg_name = args.pop(arg_pos)
if len(args) > (arg_pos + 1):
arg_val = args.pop(arg_pos)
if integer:
int_arg_val = 0
if arg_val is not None:
try:
int_arg_val = int(arg_val)
except ValueError:
print "Invalid integer parameter, defaulting to zero. Parameter name:", arg_name
arg_val = int_arg_val
return arg_val
def run():
""" main execution """
args = sys.argv[1:]
# optional device name parm. if not specified, device_name = None (the first device found will be used).
device_name = get_named_arg_value("-devicename", args)
# optional transcoder parm. if not specified, ffmpeg will be used, if installed, otherwise avconv.
transcoder = get_named_arg_value("-transcoder", args)
# optional server port parm. if not specified, a random available port will be used
server_port = get_named_arg_value("-port", args)
# optional transcode options parm. if specified, these options will be passed to the transcoder to be applied to the output
transcode_options = get_named_arg_value("-transcodeopts", args)
# optional transcode options parm. if specified, these options will be passed to the transcoder to be applied to the input data
transcode_input_options = get_named_arg_value("-transcodeinputopts", args)
# optional transcode bufsize parm. if specified, the transcoder will buffer approximately this many bytes of output
transcode_bufsize = get_named_arg_value("-transcodebufsize", args, integer=True)
# optional subtitle parm. if specified, the specified subtitles will be played.
subtitles = get_named_arg_value("-subtitles", args)
# optional subtitle_port parm. if not specified, a random available port will be used.
subtitles_port = get_named_arg_value("-subtitles_port", args)
# optional subtitle_language parm. if not specified en-US will be used.
subtitles_language = get_named_arg_value("-subtitles_language", args)
validate_args(args)
if args[0] == "-stop":
stop(device_name=device_name)
elif args[0] == "-pause":
pause(device_name=device_name)
elif args[0] == "-continue":
unpause(device_name=device_name)
elif args[0] == "-status":
get_status(device_name=device_name)
elif args[0] == "-setvol":
set_volume(float(args[1]), device_name=device_name)
elif args[0] == "-volup":
volume_up(device_name=device_name)
elif args[0] == "-voldown":
volume_down(device_name=device_name)
elif args[0] == "-mute":
set_volume(0, device_name=device_name)
elif args[0] == "-transcode":
arg2 = args[1]
play(arg2, transcode=True, transcoder=transcoder, transcode_options=transcode_options, transcode_input_options=transcode_input_options, transcode_bufsize=transcode_bufsize,
device_name=device_name, server_port=server_port, subtitles=subtitles, subtitles_port=subtitles_port,
subtitles_language=subtitles_language)
elif args[0] == "-playurl":
arg2 = args[1]
playurl(arg2, device_name=device_name)
elif args[0] == "-devicelist":
list_devices()
else:
play(args[0], device_name=device_name, server_port=server_port, subtitles=subtitles,
subtitles_port=subtitles_port, subtitles_language=subtitles_language)
if __name__ == "__main__":
run()