]> git.mdlowis.com Git - proto/atv.git/commitdiff
initial commit
authorMike Lowis <mike.lowis@gentex.com>
Thu, 21 Dec 2023 15:23:31 +0000 (10:23 -0500)
committerMike Lowis <mike.lowis@gentex.com>
Thu, 21 Dec 2023 15:23:31 +0000 (10:23 -0500)
.gitignore [new file with mode: 0644]
index.html [new file with mode: 0644]
play-videos [new file with mode: 0755]
play.html [new file with mode: 0644]
scan-videos [new file with mode: 0755]
serve [new file with mode: 0755]
serve-videos [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..94a2dd1
--- /dev/null
@@ -0,0 +1 @@
+*.json
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..a08ba8c
--- /dev/null
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html dir="ltr" lang="en-US">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <style>
+    html, body {
+      margin: 0;
+      padding: 0;
+      width: 100%;
+      height: 100%;
+      max-height: 100%;
+      background-color: #000;
+      overflow: hidden;
+    }
+  </style>
+</head>
+<body>
+<video id="Video" width="100%" height="100%" src="" autoplay muted playsinline>
+</video>
+
+  <script>
+    (()=>{
+    let program = {};
+
+    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 since_mn = secondsSinceMidnight();
+        const end_time = channel["start_time"] + duration;
+        const offset   = duration - (end_time - since_mn);
+        console.log(offset);
+        if (force || (offset > 0 && offset < duration))
+        {
+            Video.src = channel["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 updateCurrentItem = ()=>{
+        updateProgram(playNextItem);
+    };
+
+    /* start the program and setup listeners to keep it in sync */
+    updateCurrentItem();
+    Video.addEventListener("ended", updateCurrentItem);
+    document.addEventListener("visibilitychange", updateCurrentItem);
+    setInterval(updateProgram, 1000);
+})();
+  </script>
+
+</body></html>
\ No newline at end of file
diff --git a/play-videos b/play-videos
new file mode 100755 (executable)
index 0000000..37e9361
--- /dev/null
@@ -0,0 +1,89 @@
+#!/bin/env ruby
+
+require 'json'
+require 'fileutils'
+
+# reload the database
+$db = JSON.parse(File.read("index.json"))
+$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()
+  data = { "curr" => 1, "channels" => $channels.map{|c| c[:play] } }
+  pp data
+  File.open("program.json.temp", "wb") do |f|
+    f.write JSON.dump(data)
+  end
+  FileUtils.mv("program.json.temp", "program.json")
+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
+
+# load up the channels and generate the initial program
+populate_channels()
+update_program()
+
+# wake up once a second to see if a program has finished and pick the next program
+while 1
+  if program_changed()
+    update_program()
+  end
+  sleep 1
+end
+
diff --git a/play.html b/play.html
new file mode 100644 (file)
index 0000000..a08ba8c
--- /dev/null
+++ b/play.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html dir="ltr" lang="en-US">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <style>
+    html, body {
+      margin: 0;
+      padding: 0;
+      width: 100%;
+      height: 100%;
+      max-height: 100%;
+      background-color: #000;
+      overflow: hidden;
+    }
+  </style>
+</head>
+<body>
+<video id="Video" width="100%" height="100%" src="" autoplay muted playsinline>
+</video>
+
+  <script>
+    (()=>{
+    let program = {};
+
+    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 since_mn = secondsSinceMidnight();
+        const end_time = channel["start_time"] + duration;
+        const offset   = duration - (end_time - since_mn);
+        console.log(offset);
+        if (force || (offset > 0 && offset < duration))
+        {
+            Video.src = channel["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 updateCurrentItem = ()=>{
+        updateProgram(playNextItem);
+    };
+
+    /* start the program and setup listeners to keep it in sync */
+    updateCurrentItem();
+    Video.addEventListener("ended", updateCurrentItem);
+    document.addEventListener("visibilitychange", updateCurrentItem);
+    setInterval(updateProgram, 1000);
+})();
+  </script>
+
+</body></html>
\ No newline at end of file
diff --git a/scan-videos b/scan-videos
new file mode 100755 (executable)
index 0000000..de8a402
--- /dev/null
@@ -0,0 +1,49 @@
+#!/bin/env ruby
+
+require 'json'
+require 'fileutils'
+
+CMD="ffprobe -show_entries format=duration -v quiet -of csv=\"p=0\" -i"
+
+# reload the database
+$db = {}
+if File.exist?("index.json")
+  $db = JSON.parse(File.read("index.json"))
+end
+
+def cleanup_files()
+  $db.keys.each do |f|
+    if not File.exist? f then
+      $db.delete(f)
+    end
+  end
+end
+
+def scan_pictures(root_path)
+end
+
+#def scan_shows(root_path)
+#end
+
+def scan_videos(root_path)
+  Dir.glob("#{root_path}/**/*.{mp4,webm,ogg}").each do |f|
+    next if $db[f]
+    puts f
+    duration = `#{CMD} \"#{f}\"`.chomp.to_f
+    $db[f] = { "duration" => duration }
+  end
+end
+
+# scan for pictures and videos
+cleanup_files()
+scan_pictures("Pictures")
+scan_videos("Christmas")
+scan_videos("Movies")
+scan_videos("Shorts")
+scan_videos("Shows")
+
+# write the database to disk
+File.open("index.json.temp", "wb") do |f|
+  f.write JSON.dump($db)
+end
+FileUtils.mv("index.json.temp", "index.json")
diff --git a/serve b/serve
new file mode 100755 (executable)
index 0000000..db6a7cb
--- /dev/null
+++ b/serve
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+play_videos(){
+    while [ 1 ]; do
+        ./play-videos
+    done
+}
+
+play_videos &
+websocketd -staticdir . -port 8080 cat
diff --git a/serve-videos b/serve-videos
new file mode 100755 (executable)
index 0000000..cc02160
--- /dev/null
@@ -0,0 +1,26 @@
+#!/bin/env ruby
+
+require 'iodine'
+require 'fileutils'
+
+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
+end
+
+
+# static file service
+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
+Iodine.start