simple video test

This commit is contained in:
Alan Daniels 2025-11-07 22:29:51 +11:00
parent fd584886c6
commit fc4558be2c
10 changed files with 195 additions and 59 deletions

7
package-lock.json generated
View file

@ -6,6 +6,7 @@
"": { "": {
"dependencies": { "dependencies": {
"@inertiajs/vue3": "^2.2.11", "@inertiajs/vue3": "^2.2.11",
"hls.js": "^1.6.14",
"tailwindcss": "^4.1.16" "tailwindcss": "^4.1.16"
}, },
"devDependencies": { "devDependencies": {
@ -2134,6 +2135,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/hls.js": {
"version": "1.6.14",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.14.tgz",
"integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==",
"license": "Apache-2.0"
},
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.4", "version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",

View file

@ -6,6 +6,7 @@
}, },
"dependencies": { "dependencies": {
"@inertiajs/vue3": "^2.2.11", "@inertiajs/vue3": "^2.2.11",
"hls.js": "^1.6.14",
"tailwindcss": "^4.1.16" "tailwindcss": "^4.1.16"
} }
} }

View file

@ -6,30 +6,31 @@ const Allocator = std.mem.Allocator;
const builtin = @import("builtin"); const builtin = @import("builtin");
const optimize_mode = builtin.mode; const optimize_mode = builtin.mode;
const model_cat = @import("./models/cat.zig");
const model_tag = @import("./models/tag.zig"); const model_tag = @import("./models/tag.zig");
db_connection: *sqlite.Db, db_connection: *sqlite.Db,
counter: u32 = 0, counter: u32 = 0,
pub fn init(connection: *sqlite.Db) !@This() { pub fn init(connection: *sqlite.Db) !@This() {
try connection.execDynamic(model_tag.schema, .{}, .{}); // try connection.execDynamic(model_cat.schema, .{}, .{});
// try connection.execDynamic(model_tag.schema, .{}, .{});
return .{ return .{
.db_connection = connection, .db_connection = connection,
}; };
} }
pub fn SendInertiaResponse(endpoint: anytype, context: *@This(), r: zap.Request, arena: Allocator, component: []const u8, props: anytype) !void { pub fn SendInertiaResponse(endpoint: anytype, context: *@This(), r: zap.Request, arena: Allocator, component: []const u8, props: anytype) !void {
const tags = try model_tag.getTopLevelTags(context.db_connection, arena); _ = context;
// const tags = try model_tag.getTopLevelTags(context.db_connection, arena);
// const cats = try model_cat.getTopLevelcats(context.db_connection, arena);
const inertia_json = try std.fmt.allocPrint( const inertia_json = try std.fmt.allocPrint(
arena, arena,
"{f}", "{f}",
.{std.json.fmt(.{ .{std.json.fmt(.{
.component = component, .component = component,
.props = .{ .props = props,
.tags = tags,
.page = props,
},
.url = endpoint.path, .url = endpoint.path,
.version = "", .version = "",
}, .{ }, .{
@ -41,7 +42,6 @@ pub fn SendInertiaResponse(endpoint: anytype, context: *@This(), r: zap.Request,
try r.setHeader("X-Inertia", "true"); try r.setHeader("X-Inertia", "true");
try r.setHeader("Content-Type", "application/json"); try r.setHeader("Content-Type", "application/json");
r.setStatus(.ok);
try r.sendBody(inertia_json); try r.sendBody(inertia_json);
} else { } else {
try r.setHeader("Content-Type", "text/html"); try r.setHeader("Content-Type", "text/html");
@ -68,7 +68,6 @@ pub fn SendInertiaResponse(endpoint: anytype, context: *@This(), r: zap.Request,
}, },
); );
r.setStatus(.ok);
try r.sendBody(response_text); try r.sendBody(response_text);
} }
} }
@ -91,5 +90,6 @@ pub const HtmlEncodeFormatter = struct {
pub fn unhandledRequest(ctx: *@This(), arena: Allocator, r: zap.Request) anyerror!void { pub fn unhandledRequest(ctx: *@This(), arena: Allocator, r: zap.Request) anyerror!void {
const path = r.path orelse "/not-found"; const path = r.path orelse "/not-found";
r.setStatus(.not_found);
try SendInertiaResponse(.{ .path = path }, ctx, r, arena, "404", .{}); try SendInertiaResponse(.{ .path = path }, ctx, r, arena, "404", .{});
} }

View file

@ -30,6 +30,7 @@ const SimpleEndpoint = struct {
pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *Context, r: zap.Request) !void { pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *Context, r: zap.Request) !void {
const thread_id = std.Thread.getCurrentId(); const thread_id = std.Thread.getCurrentId();
r.setStatus(.ok);
try Context.SendInertiaResponse(e, context, r, arena, e.component, .{ try Context.SendInertiaResponse(e, context, r, arena, e.component, .{
.thread_id = thread_id, .thread_id = thread_id,
.counter = context.counter, .counter = context.counter,
@ -85,12 +86,14 @@ pub fn main() !void {
// create the endpoints // create the endpoints
var home_endpoint = SimpleEndpoint.init("/", "home/home"); var home_endpoint = SimpleEndpoint.init("/", "home/home");
var test_endpoint = SimpleEndpoint.init("/test", "home/test"); var test_endpoint = SimpleEndpoint.init("/test", "home/test");
var test_video_endpoint = SimpleEndpoint.init("/video", "media/video");
var stop_endpoint: StopEndpoint = .{ .path = "/stop" }; var stop_endpoint: StopEndpoint = .{ .path = "/stop" };
// register the endpoints with the App // register the endpoints with the App
try App.register(&stop_endpoint); try App.register(&stop_endpoint);
try App.register(&home_endpoint); try App.register(&home_endpoint);
try App.register(&test_endpoint); try App.register(&test_endpoint);
try App.register(&test_video_endpoint);
// listen on the network // listen on the network
try App.listen(.{ try App.listen(.{

47
src/models/cat.zig Normal file
View file

@ -0,0 +1,47 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const optimize_mode = builtin.mode;
const sqlite = @import("sqlite");
pub const schema =
\\CREATE TABLE IF NOT EXISTS cat(
\\ id INTEGER PRIMARY KEY AUTOINCREMENT,
\\ key TEXT UNIQUE NOT NULL,
\\ name TEXT NOT NULL
\\);
;
id: usize,
key: []const u8,
name: []const u8,
const ArrList = std.ArrayList(@This());
pub fn getTopLevelcats(conn: *sqlite.Db, arena: Allocator) ![]@This() {
var stmt = try conn.prepareDynamic("SELECT id, key, name FROM cat WHERE TRUE;");
defer stmt.deinit();
var list = ArrList.empty;
defer list.deinit(arena);
var iter = try stmt.iterator(@This(), .{});
while (try iter.nextAlloc(arena, .{})) |row| {
try list.append(arena, row);
}
return list.toOwnedSlice(arena);
}
pub fn jsonStringify(v: @This(), jws: anytype) !void {
try jws.beginObject();
try jws.objectField("key");
try jws.write(v.key);
try jws.objectField("name");
try jws.write(v.name);
try jws.endObject();
}

View file

@ -5,77 +5,43 @@ const optimize_mode = builtin.mode;
const sqlite = @import("sqlite"); const sqlite = @import("sqlite");
const Self = @This();
pub const schema = pub const schema =
\\CREATE TABLE IF NOT EXISTS tag( \\CREATE TABLE IF NOT EXISTS tag(
\\ id INTEGER PRIMARY KEY AUTOINCREMENT, \\ id INTEGER PRIMARY KEY AUTOINCREMENT,
\\ key TEXT UNIQUE NOT NULL, \\ key TEXT UNIQUE NOT NULL,
\\ parent_key TEXT TEXT, \\ cat_id INTEGER NOT NULL,
\\ FOREIGN KEY (parent_key) REFERENCES tag(key) ON DELETE RESTRICT \\ FOREIGN KEY (cat_id) REFERENCES cat(id) ON DELETE RESTRICT
\\); \\);
; ;
id: usize, id: usize,
cat_id: usize,
key: []const u8, key: []const u8,
parent: ?*@This() = null,
children: ?[]@This() = null,
const internal = struct { const ArrList = std.ArrayList(Self);
id: usize,
key: []const u8,
parent_key: ?[]const u8,
};
const ArrList = std.ArrayList(@This()); pub fn getTopLevelTags(conn: *sqlite.Db, arena: Allocator) ![]Self {
var stmt = try conn.prepareDynamic("SELECT id, cat_id, key FROM tag WHERE TRUE;");
pub fn getTopLevelTags(conn: *sqlite.Db, arena: Allocator) ![]@This() {
var stmt = try conn.prepareDynamic("SELECT id, key, parent_key FROM tag WHERE parent_key IS NULL;");
defer stmt.deinit(); defer stmt.deinit();
var list = ArrList.empty; var list = ArrList.empty;
defer list.deinit(arena); defer list.deinit(arena);
var iter = try stmt.iterator(internal, .{}); var iter = try stmt.iterator(Self, .{});
while (try iter.nextAlloc(arena, .{})) |row| { while (try iter.nextAlloc(arena, .{})) |row| {
var tag: @This() = .{ try list.append(arena, row);
.id = row.id,
.key = row.key,
};
try tag.getChildren(conn, arena);
try list.append(arena, tag);
} }
return list.toOwnedSlice(arena); return list.toOwnedSlice(arena);
} }
pub fn getChildren(self: *@This(), conn: *sqlite.Db, arena: Allocator) !void { pub fn jsonStringify(v: Self, jws: anytype) !void {
var stmt = try conn.prepareDynamic("SELECT id, key, parent_key FROM tag WHERE parent_key LIKE ?;");
defer stmt.deinit();
var list = ArrList.empty;
defer list.deinit(arena);
var iter = try stmt.iterator(internal, .{self.key});
while (try iter.nextAlloc(arena, .{})) |row| {
var tag: @This() = .{
.id = row.id,
.key = row.key,
};
tag.parent = self;
try tag.getChildren(conn, arena);
try list.append(arena, tag);
}
self.children = try list.toOwnedSlice(arena);
}
pub fn jsonStringify(v: @This(), jws: anytype) !void {
try jws.beginObject(); try jws.beginObject();
try jws.objectField("key"); try jws.objectField("key");
try jws.write(v.key); try jws.write(v.key);
try jws.objectField("children");
try jws.write(v.children);
try jws.endObject(); try jws.endObject();
} }

View file

@ -2,8 +2,7 @@
import Layout from "@/layout/Layout.vue"; import Layout from "@/layout/Layout.vue";
import { Head, Link } from "@inertiajs/vue3"; import { Head, Link } from "@inertiajs/vue3";
const props = defineProps<{ page: object }>(); const props = defineProps<{ thread_id, counter }>();
const { thread_id, counter } = props.page;
</script> </script>
<template> <template>

View file

@ -2,8 +2,7 @@
import Layout from "@/layout/Layout.vue"; import Layout from "@/layout/Layout.vue";
import { Head, Link } from "@inertiajs/vue3"; import { Head, Link } from "@inertiajs/vue3";
const props = defineProps<{ page: object }>(); const props = defineProps<{ thread_id, counter }>();
const { thread_id, counter } = props.page;
</script> </script>
<template> <template>

View file

@ -0,0 +1,81 @@
<template>
<video
@pause="pause"
@ended="pause"
@keyup="changeSpeed"
ref="video"
:poster="previewImageLink"
:controls="isControls"
:title="title"
>
<source :src="link" type="application/x-mpegURL" />
</video>
</template>
<script setup>
import { onMounted, onUpdated, ref } from "vue";
import Hls from "hls.js";
const props = defineProps({
previewImageLink: {
type: String,
default: "",
},
link: {
type: String,
default: "",
},
progress: {
type: Number,
default: 0,
},
title: {
type: String,
default: "",
},
isMuted: {
type: Boolean,
default: false,
},
isControls: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(["pause", "test"]);
const video = ref(null);
onMounted(() => {
prepareVideoPlayer();
});
onUpdated(() => {
prepareVideoPlayer();
});
function prepareVideoPlayer() {
let hls = new Hls();
let stream = props.link;
hls.loadSource(stream);
if (video.value) {
hls.attachMedia(video.value);
video.value.muted = props.isMuted;
video.value.currentTime = props.progress;
}
}
function pause() {
const currentTime = video?.value?.currentTime || 0;
emit("pause", currentTime);
}
function changeSpeed(e) {
if (e.key === "w" && video.value) {
video.value.playbackRate = video.value.playbackRate + 0.25;
} else if (e.key === "s" && video.value) {
video.value.playbackRate = video.value.playbackRate - 0.25;
}
}
</script>

View file

@ -0,0 +1,33 @@
<script setup lang="ts">
import Layout from "@/layout/Layout.vue";
import { Head, Link } from "@inertiajs/vue3";
import BasePlayer from "./BasePlayer.vue";
const props = defineProps<{ thread_id; counter }>();
function pause(currentTime) {
console.log("PAUSE", currentTime);
}
const link = '/test.m3u8';
const previewImageLink = '/test.jpg';
const isMuted = false;
const isControls = true;
const progress_S = 15;
</script>
<template>
<Layout>
<div class="bg-gray-800">
<BasePlayer
:previewImageLink="previewImageLink"
:link="link"
:progress="progress_S"
:isMuted="isMuted"
:isControls="isControls"
@pause="pause"
class="aspect-video bg-black mx-auto max-h-[80vh]"
/>
</div>
</Layout>
</template>