<video id="Video" width="100%" height="100%" src="" autoplay muted playsinline>
</video>
- <script>
- (()=>{
- let program = {};
+<script>
- const secondsSinceMidnight = ()=>{
- const now = new Date();
- const midnight = new Date(
- now.getFullYear(),
- now.getMonth(),
- now.getDate(),
- 0,0,0);
- return (now.getTime() - midnight.getTime()) / 1000;
- };
+const secondsSinceMidnight = ()=>{
+ const now = new Date();
+ const midnight = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ 0,0,0);
+ return (now.getTime() - midnight.getTime()) / 1000;
+};
- const playNextItem = (force)=>{
- const channel = program["channels"][program["curr"]];
- console.log(channel);
- const duration = channel["curr"]["duration"];
+const Cmd = {
+ play: (data)=>{
+ console.log(data);
+ const duration = data["curr"]["duration"];
const since_mn = secondsSinceMidnight();
- const end_time = channel["start_time"] + duration;
+ const end_time = data["start_time"] + duration;
const offset = duration - (end_time - since_mn);
console.log(offset);
- if (force || (offset > 0 && offset < duration))
+ if (offset > 0 && offset < duration)
{
- Video.src = channel["curr"]["path"] + "#t=" + Math.floor(offset);
+ Video.src = data["curr"]["path"] + "#t=" + Math.floor(offset);
console.log(Video.src);
}
- };
+ }
+};
- const updateProgram = (onload)=>{
- fetch("program.json")
- .then((resp)=> resp.json() )
- .then((json)=>{
- let changed = (
- program["curr"] !== json["curr"]
- );
- program = json;
- if (typeof onload === 'function') {
- onload();
- } else if (changed) {
- playNextItem();
- }
- });
+const connect = ()=>{
+ let ws = new WebSocket("ws://" + window.location.host);
+ ws.onmessage = (event)=>{
+ const msg = JSON.parse(event.data);
+ Cmd[msg.cmd](msg);
};
- const updateCurrentItem = ()=>{
- updateProgram(playNextItem);
+ ws.onclose = (event)=>{
+ connect();
};
+};
+
- /* start the program and setup listeners to keep it in sync */
- updateCurrentItem();
- Video.addEventListener("ended", updateCurrentItem);
- document.addEventListener("visibilitychange", updateCurrentItem);
- setInterval(updateProgram, 1000);
-})();
- </script>
+(()=>{ connect(); })();
+</script>
</body></html>
\ No newline at end of file
#!/bin/env ruby
require 'iodine'
+require 'json'
require 'fileutils'
+# reload the database
+$db = JSON.parse(File.read("index.json"))
+$now_playing = nil
+$channel = 0
+$channels = [
+ { include: ["Christmas"] },
+ { include: ["Movies", "Shorts", "Shows"] },
+ { include: ["Movies"] },
+ { include: ["Shorts"] },
+ { include: ["Shows"] },
+ { include: ["Pictures"] },
+]
+
+def secs_since_midnight()
+ now = Time.now
+ midnight = Time.new(now.year, now.month, now.day, 0, 0, 0)
+ (now - midnight)
+end
+
+def next_show(channel)
+ cfg = $channels[channel]
+ path = cfg[:files][cfg[:curr]]
+ cfg[:curr] += 1
+ if cfg[:curr] >= cfg[:files].length
+ cfg[:files].shuffle
+ cfg[:curr] = 0
+ end
+ (path ? $db[path].merge({"path"=>path}) : nil)
+end
+
+def populate_channels()
+ files = $db.keys
+ start_time = secs_since_midnight()
+ $channels.each_with_index do |cfg, i|
+ cfg[:files] = files.select{|f| cfg[:include].select{|prefix| f.start_with? prefix}.length > 0 }.shuffle
+ cfg[:curr] = 0
+ cfg[:play] = {
+ chan: i,
+ start_time: start_time,
+ }
+ cfg[:play][:curr] = next_show(i)
+ cfg[:play][:next] = next_show(i)
+ end
+end
+
+def update_program()
+ $now_playing = $channels[$channel][:play]
+ Iodine.publish(:atv, JSON.dump($now_playing.merge({ "cmd" => "play" })))
+end
+
+def program_changed()
+ changed = false
+ time = secs_since_midnight
+# time = $channels[0][:play][:start_time] + $channels[0][:play][:curr]["duration"]
+ $channels.each_with_index do |chan, i|
+ next if not chan[:play][:curr]
+ end_time = chan[:play][:start_time] + chan[:play][:curr]["duration"]
+ if end_time <= time then
+ changed = true
+ chan[:play][:chan] = i
+ chan[:play][:curr] = chan[:play][:next]
+ chan[:play][:next] = next_show(i)
+ chan[:play][:start_time] = time
+# pp chan[:play]
+ end
+# puts "Chan #{i} time remaining #{end_time - time}"
+ end
+ changed
+end
+
+module ATV
+ def on_open(client)
+ puts "connect: #{client}"
+ client.subscribe :atv
+ send(client, $now_playing.merge({ "cmd" => "play" }))
+ end
+
+ def on_close(client)
+ puts "disconnect: #{client}"
+ end
+
+ def on_message(client, data)
+ # nothing to do, we are stream only
+ end
+
+ def send(client, data)
+ client.write JSON.dump(data)
+ end
+
+ extend self
+end
+
APP = Proc.new do |env|
-# if env['rack.upgrade?'.freeze] == :websocket
-# env['rack.upgrade'.freeze] = SystemTrace
-# [0,{}, []] # It's possible to set cookies for the response.
-# else
-# data = PAGE_HTML
-# [200, {"Content-Length" => "#{data.length}", "Content-Type" => "text/html"}, [data]]
-# end
+ if env['rack.upgrade?'.freeze] == :websocket
+ env['rack.upgrade'.freeze] = ATV
+ [0,{}, []] # It's possible to set cookies for the response.
+ end
end
+# load up the channels and generate the initial program
+populate_channels()
+update_program()
-# static file service
+# setup the websocket and static file server
Iodine.listen(
service: :http,
- public: Dir.getwd(),
handler: APP
)
-# for static file service, we only need a single thread and a single worker.
-Iodine.threads = 1
+# Every second check for a video change and send it out if needed
+Iodine.run_every(1000) do
+ if program_changed()
+ update_program()
+ end
+end
+
+# start the server now
Iodine.start