├── yt2chromecast.py
└── usr
│ └── bin
│ └── video2chromecast.py
├── LICENSE
├── README.md
└── video2smarttv.py
/yt2chromecast.py/usr/bin/video2chromecast.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | import time, sys
4 |
5 | if(len(sys.argv) > 0):
6 | video = sys.argv[1]
7 | else:
8 | video = "REjj1ruFQww"
9 |
10 | try:
11 | import pychromecast
12 | pychromecast.play_youtube_video(video, pychromecast.PyChromecast().host)
13 | except:
14 | pass
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 probonopd
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | video2smarttv
2 | =============
3 |
4 | Send mp4 video URLs or YouTube videos to Samsung Smart TV using UPnP, DLNA. This is a no-frills implementation intended to be as lightweight and portable as possible. Hence, it does not use any XML, SOAP, or UPnP libraries. It locates devices with SSDP and sends to any it can find. Might work with other UPnP Smart TVs too.
5 |
6 | Prerequisites
7 | -------------
8 |
9 | Python 2.7, should come preinstalled with OS X and most Linux distributions. Optionally, [youtube-dl](http://rg3.github.io/youtube-dl/download.html).
10 |
11 | Installation
12 | ------------
13 |
14 | Just download and make executable. This is just one Python file.
15 |
16 | ```
17 | wget https://raw.githubusercontent.com/probonopd/video2smarttv/master/video2smarttv.py
18 | chmod a+x ./video2smarttv.py
19 | sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl # Optional
20 | sudo chmod a+rx /usr/local/bin/youtube-dl # Optional
21 | ````
22 |
23 | Usage
24 | -----
25 |
26 | Send a mp4 video file to the Smart TV for playing by specifying the IP address of the TV and the URL to be played:
27 |
28 | ```
29 | ./video2smarttv.py http://techslides.com/demos/sample-videos/small.mp4
30 | ````
31 |
32 | If you have youtube-dl installed, you can also use YouTube IDs or search keywords instead:
33 |
34 | ```
35 | ./video2smarttv.py BaW_jenozKc
36 | ./video2smarttv.py some youtube search
37 | ```
38 |
--------------------------------------------------------------------------------
/video2smarttv.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # coding: utf-8
3 |
4 | #
5 | # Send mp4 streaming media URIs to Samsung Smart TV for immediate playback
6 | #
7 |
8 | import os, socket, argparse, logging, subprocess, cgi, httplib, StringIO, urllib2, re, urlparse
9 |
10 | #
11 | # Function to discover services on the network using SSDP
12 | # Inspired by https://gist.github.com/dankrause/6000248
13 | #
14 |
15 | class SsdpFakeSocket(StringIO.StringIO):
16 | def makefile(self, *args, **kw): return self
17 |
18 | def ssdp_discover(service):
19 | group = ("239.255.255.250", 1900)
20 | message = "\r\n".join(['M-SEARCH * HTTP/1.1', 'HOST: {0}:{1}', 'MAN: "ssdp:discover"', 'ST: {st}','MX: 3','',''])
21 | socket.setdefaulttimeout(0.5)
22 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
23 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
24 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
25 | sock.sendto(message.format(*group, st=service), group)
26 | results = []
27 | while True:
28 | try:
29 | r = httplib.HTTPResponse(SsdpFakeSocket(sock.recv(1024)))
30 | r.begin()
31 | results.append(r.getheader("location"))
32 | except socket.timeout:
33 | break
34 | return results
35 |
36 | #
37 | # DIDL-Lite template
38 | # Note that this is included in the Universal Plug and Play (UPnP) message in urlencoded form
39 | #
40 |
41 | didl_lite_template = """
42 | -
43 | object.item.videoItem
44 | $$$URI$$$
45 |
46 | """
47 |
48 | # Remove newlines and whitespace
49 | didl_lite = ' '.join(cgi.escape(didl_lite_template.replace("\n","")).split())
50 |
51 | #
52 | # Universal Plug and Play (UPnP) message templates.
53 | # Note that the request times out if there is a blank charater between the header lines and the message
54 | # or if we do not use .replace("\n", "\r\n") to get "CRLF line terminators"
55 | #
56 |
57 | AVTransportTemplate = """POST $$$APIURL$$$ HTTP/1.1
58 | Accept: application/json, text/plain, */*
59 | Soapaction: "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"
60 | Content-Type: text/xml;charset="UTF-8"
61 |
62 |
63 |
64 |
65 |
66 | 0
67 | $$$URI$$$
68 | $DIDL
69 |
70 |
71 | """
72 |
73 | PlayTemplate = """POST $$$APIURL$$$ HTTP/1.1
74 | Accept: application/json, text/plain, */*
75 | Soapaction: "urn:schemas-upnp-org:service:AVTransport:1#Play"
76 | Content-Type: text/xml;charset="UTF-8"
77 |
78 |
79 |
80 |
81 |
82 | 0
83 | 1
84 |
85 |
86 | """
87 |
88 | #
89 | # Send message to the TV
90 | #
91 |
92 | def sendMessage(ip, port, message):
93 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
94 | s.connect((ip, port))
95 | sent = s.send(message.replace("\n", "\r\n"))
96 | if (sent <= 0):
97 | print("Error sending message")
98 | s.close()
99 | return
100 | recv = s.recv(100000)
101 | s.close()
102 | logging.debug(recv)
103 | logging.debug("")
104 |
105 | #
106 | # Parse command line and send messages to the TV
107 | #
108 |
109 | def main():
110 | parser = argparse.ArgumentParser(description='Send mp4 video streams to a Samsung Smart TV', add_help = True)
111 | flags = parser.add_argument_group('Arguments')
112 | parser.add_argument("-v", "--verbose", help="Verbose output, print requests and responses", action="store_true")
113 | flags.add_argument('uri', nargs='+', default = None, help = 'Required. URI to be sent to TV. If this does not start with http, it is sent to yt-downloader for processing.')
114 |
115 | args = parser.parse_args()
116 | if args.verbose:
117 | logging.basicConfig(level=logging.DEBUG)
118 |
119 | # Find TV devices using SSDP
120 | tvs = []
121 | results = ssdp_discover("urn:schemas-upnp-org:service:AVTransport:1")
122 | for result in results:
123 | logging.debug(result)
124 | data = urllib2.urlopen(result).read()
125 | # logging.debug(data)
126 | expr = re.compile(r"urn:upnp-org:serviceId:AVTransport.*(.*)", re.DOTALL)
127 | regexresult = expr.findall(data)
128 | logging.debug(regexresult)
129 | o = urlparse.urlparse(result)
130 | tv = {"ip": o.hostname, "port": o.port, "url": regexresult[0]}
131 | logging.debug(tv)
132 | tvs.append(tv)
133 |
134 | # Do a search if required
135 | if not (args.uri[0].startswith("http")):
136 | myexec = "youtube-dl"
137 | try:
138 | FNULL = open(os.devnull, 'w')
139 | subprocess.call([myexec, '--version'], stdout=FNULL, stderr=FNULL)
140 | except OSError:
141 | print "%s is not installed." % myexec
142 | print "Install it in order to be able to search YouTube."
143 | exit(1)
144 | command = ["youtube-dl", "-f", "mp4", "-g", "--default-search", "auto", " ".join(args.uri)]
145 | logging.debug(command)
146 | process = subprocess.Popen(command, stdout=subprocess.PIPE)
147 | out, err = process.communicate()
148 | logging.debug(out)
149 | args.uri = out.strip()
150 | else:
151 | args.uri = args.uri[0]
152 |
153 | for tv in tvs:
154 | message = AVTransportTemplate.replace("$DIDL", didl_lite).replace("$$$URI$$$", args.uri).replace("$$$APIURL$$$", tv["url"])
155 | logging.debug(message)
156 | sendMessage(tv["ip"], tv["port"], message)
157 | message = PlayTemplate.replace("$$$APIURL$$$", tv["url"])
158 | logging.debug(message)
159 | sendMessage(tv["ip"], tv["port"], message)
160 |
161 | main()
162 |
--------------------------------------------------------------------------------