From 2dd33e4ea64f226a3c3ff849fff640b23a46d408 Mon Sep 17 00:00:00 2001 From: LordMZTE Date: Tue, 9 Aug 2022 12:26:33 +0200 Subject: [PATCH] improve playtwitch --- scripts/playtwitch/src/ffi.zig | 10 ++ scripts/playtwitch/src/gui.zig | 231 +++++++++++++++++++++++--------- scripts/playtwitch/src/main.zig | 47 ------- 3 files changed, 175 insertions(+), 113 deletions(-) diff --git a/scripts/playtwitch/src/ffi.zig b/scripts/playtwitch/src/ffi.zig index 650b04e..e5d38de 100644 --- a/scripts/playtwitch/src/ffi.zig +++ b/scripts/playtwitch/src/ffi.zig @@ -1,4 +1,5 @@ // partially yoinked from https://github.com/Swoogan/ziggtk +const std = @import("std"); pub const c = @cImport({ @cInclude("gtk/gtk.h"); }); @@ -18,3 +19,12 @@ pub fn connectSignal( pub fn getEntryBufferText(buf: *c.GtkEntryBuffer) []const u8 { return c.gtk_entry_buffer_get_text(buf)[0..c.gtk_entry_buffer_get_length(buf)]; } + +pub fn handleGError(err: *?*c.GError) !void { + if (err.*) |e| { + std.log.err("glib error: {s}", .{e.message}); + c.g_error_free(e); + err.* = null; + return error.GError; + } +} diff --git a/scripts/playtwitch/src/gui.zig b/scripts/playtwitch/src/gui.zig index e8d33ea..b1ed3af 100644 --- a/scripts/playtwitch/src/gui.zig +++ b/scripts/playtwitch/src/gui.zig @@ -1,14 +1,13 @@ const std = @import("std"); +const c_allocator = std.heap.c_allocator; const ffi = @import("ffi.zig"); const c = ffi.c; +var chatty_alive = false; + pub const GuiState = struct { - alloc: std.mem.Allocator, /// An arena allocator used to store userdata for widgets of the UI udata_arena: std.mem.Allocator, - - streamlink_child: ?std.ChildProcess = null, - chatty_child: ?std.ChildProcess = null, }; pub fn activate(app: *c.GtkApplication, state: *GuiState) void { @@ -61,6 +60,9 @@ pub fn activate(app: *c.GtkApplication, state: *GuiState) void { const other_stream_entry = c.gtk_entry_new_with_buffer(other_stream_buffer); c.gtk_box_append(@ptrCast(*c.GtkBox, content), other_stream_entry); + const dialog_buf = c.gtk_text_buffer_new(null); + const dialog = streamlinkErrorDialog(@ptrCast(*c.GtkWindow, win), dialog_buf); + c.gtk_entry_set_placeholder_text( @ptrCast(*c.GtkEntry, other_stream_entry), "Other Channel...", @@ -72,6 +74,8 @@ pub fn activate(app: *c.GtkApplication, state: *GuiState) void { .win = @ptrCast(*c.GtkWindow, win), .chatty_switch = @ptrCast(*c.GtkSwitch, chatty_switch), .quality_box = @ptrCast(*c.GtkComboBoxText, quality_box), + .dialog = dialog, + .text_buf = dialog_buf, }; ffi.connectSignal( @@ -105,16 +109,18 @@ pub fn activate(app: *c.GtkApplication, state: *GuiState) void { .win = @ptrCast(*c.GtkWindow, win), .chatty_switch = @ptrCast(*c.GtkSwitch, chatty_switch), .quality_box = @ptrCast(*c.GtkComboBoxText, quality_box), + .dialog = dialog, + .text_buf = dialog_buf, }; ffi.connectSignal(list, "row-activated", @ptrCast(c.GCallback, onRowActivate), act_data); channels: { - const channels_data = readChannels(state.alloc) catch |e| { + const channels_data = readChannels() catch |e| { std.log.err("Failed to read channels: {}", .{e}); break :channels; }; - defer state.alloc.free(channels_data); + defer c_allocator.free(channels_data); var name_buf: [64]u8 = undefined; @@ -135,13 +141,13 @@ pub fn activate(app: *c.GtkApplication, state: *GuiState) void { c.gtk_widget_show(win); } -fn readChannels(alloc: std.mem.Allocator) ![]u8 { +fn readChannels() ![]u8 { const home = try std.os.getenv("HOME") orelse error.HomeNotSet; - const fname = try std.fmt.allocPrint(alloc, "{s}/.config/playtwitch/channels", .{home}); - defer alloc.free(fname); + const fname = try std.fmt.allocPrint(c_allocator, "{s}/.config/playtwitch/channels", .{home}); + defer c_allocator.free(fname); std.log.info("Reading channels from {s}", .{fname}); const file = try std.fs.cwd().openFile(fname, .{}); - return try file.readToEndAlloc(alloc, 1024 * 1024 * 5); + return try file.readToEndAlloc(c_allocator, 1024 * 1024 * 5); } const RowActivateData = struct { @@ -149,6 +155,8 @@ const RowActivateData = struct { win: *c.GtkWindow, chatty_switch: *c.GtkSwitch, quality_box: *c.GtkComboBoxText, + dialog: *c.GtkWidget, + text_buf: *c.GtkTextBuffer, }; fn onRowActivate(list: *c.GtkListBox, row: *c.GtkListBoxRow, data: *RowActivateData) void { @@ -159,13 +167,15 @@ fn onRowActivate(list: *c.GtkListBox, row: *c.GtkListBoxRow, data: *RowActivateD defer c.g_free(quality); start( - data.state, if (c.gtk_switch_get_active(data.chatty_switch) == 0) false else true, std.mem.span(channel_name), std.mem.span(quality), + data.dialog, + data.text_buf, + data.win, ) catch |err| std.log.err("Failed to start children: {}", .{err}); - c.gtk_window_close(data.win); + c.gtk_widget_hide(@ptrCast(*c.GtkWidget, data.win)); } const OtherStreamActivateData = struct { @@ -174,6 +184,8 @@ const OtherStreamActivateData = struct { win: *c.GtkWindow, chatty_switch: *c.GtkSwitch, quality_box: *c.GtkComboBoxText, + dialog: *c.GtkWidget, + text_buf: *c.GtkTextBuffer, }; fn onOtherStreamActivate(entry: *c.GtkEntry, data: *OtherStreamActivateData) void { @@ -182,60 +194,26 @@ fn onOtherStreamActivate(entry: *c.GtkEntry, data: *OtherStreamActivateData) voi defer c.g_free(quality); start( - data.state, if (c.gtk_switch_get_active(data.chatty_switch) == 0) false else true, ffi.getEntryBufferText(data.buf), std.mem.span(quality), + data.dialog, + data.text_buf, + data.win, ) catch |err| std.log.err("Failed to start children: {}", .{err}); - c.gtk_window_close(data.win); + c.gtk_widget_hide(@ptrCast(*c.GtkWidget, data.win)); } -fn start( - state: *GuiState, - chatty: bool, - channel: []const u8, - quality: []const u8, -) !void { - if (channel.len == 0) { - std.log.warn("Exiting due to attempt to start empty channel", .{}); - return; - } - - const channel_d = try state.udata_arena.dupe(u8, channel); - const quality_d = try state.udata_arena.dupe(u8, quality); - - std.log.info( - "Starting for channel {s} with quality {s} (chatty: {})", - .{ channel_d, quality_d, chatty }, - ); - const url = try std.fmt.allocPrint(state.udata_arena, "https://twitch.tv/{s}", .{channel_d}); - const streamlink_argv = [_][]const u8{ "streamlink", url, quality_d }; - state.streamlink_child = std.ChildProcess.init( - try state.udata_arena.dupe([]const u8, &streamlink_argv), - state.alloc, - ); - - if (chatty) { - const chatty_argv = [_][]const u8{ "chatty", "-connect", "-channel", channel_d }; - state.chatty_child = std.ChildProcess.init( - try state.udata_arena.dupe([]const u8, &chatty_argv), - state.alloc, - ); - } -} - -pub fn showStreamlinkErrorDialog(output: []const u8) void { - // TODO: instead of creating a new main loop, reuse the one used for the rest of the GUI - const main_loop = c.g_main_loop_new(null, 0); - defer c.g_main_loop_unref(main_loop); - +pub fn streamlinkErrorDialog(parent_window: *c.GtkWindow, output: *c.GtkTextBuffer) *c.GtkWidget { const dialog = c.gtk_dialog_new_with_buttons( "Streamlink Crashed!", - null, + parent_window, c.GTK_DIALOG_MODAL, "_Close", c.GTK_RESPONSE_CLOSE, + "_Cancel", + c.GTK_RESPONSE_REJECT, @as(?*anyopaque, null), ); @@ -243,7 +221,7 @@ pub fn showStreamlinkErrorDialog(output: []const u8) void { dialog, "response", @ptrCast(c.GCallback, onErrorDialogResponse), - main_loop, + parent_window, ); const content = c.gtk_dialog_get_content_area(@ptrCast(*c.GtkDialog, dialog)); @@ -257,21 +235,142 @@ pub fn showStreamlinkErrorDialog(output: []const u8) void { c.gtk_label_new("Streamlink Crashed! This is the output."), ); - const output_buf = c.gtk_text_buffer_new(null); - var start_iter: c.GtkTextIter = undefined; - c.gtk_text_buffer_get_start_iter(output_buf, &start_iter); - c.gtk_text_buffer_insert(output_buf, &start_iter, output.ptr, @intCast(c_int, output.len)); - - const output_view = c.gtk_text_view_new_with_buffer(output_buf); + const output_view = c.gtk_text_view_new_with_buffer(output); c.gtk_widget_set_hexpand(output_view, 1); c.gtk_text_view_set_editable(@ptrCast(*c.GtkTextView, output_view), 0); c.gtk_box_append(@ptrCast(*c.GtkBox, content), output_view); - c.gtk_widget_show(dialog); - - c.g_main_loop_run(main_loop); + return dialog; } -fn onErrorDialogResponse(_: *c.GtkDialog, _: c_int, loop: *c.GMainLoop) void { - c.g_main_loop_quit(loop); +fn onErrorDialogResponse(dialog: *c.GtkDialog, response_id: c_int, window: *c.GtkWindow) void { + switch (response_id) { + c.GTK_RESPONSE_DELETE_EVENT, c.GTK_RESPONSE_REJECT => { + c.gtk_window_close(window); + }, + c.GTK_RESPONSE_CLOSE => { + c.gtk_widget_hide(@ptrCast(*c.GtkWidget, dialog)); + c.gtk_widget_show(@ptrCast(*c.GtkWidget, window)); + }, + else => {}, + } +} + +fn start( + chatty: bool, + channel: []const u8, + quality: []const u8, + dialog: *c.GtkWidget, + text_buf: *c.GtkTextBuffer, + window: *c.GtkWindow, +) !void { + if (channel.len == 0) { + std.log.warn("Exiting due to attempt to start empty channel", .{}); + return; + } + + var err: ?*c.GError = null; + + std.log.info( + "Starting for channel {s} with quality {s} (chatty: {})", + .{ channel, quality, chatty }, + ); + const url = try std.fmt.allocPrintZ(c_allocator, "https://twitch.tv/{s}", .{channel}); + defer c_allocator.free(url); + const quality_z = try std.cstr.addNullByte(c_allocator, quality); + defer c_allocator.free(quality_z); + const streamlink_argv = [_][*c]const u8{ "streamlink", url, quality_z, null }; + const streamlink_subproc = c.g_subprocess_newv( + &streamlink_argv, + c.G_SUBPROCESS_FLAGS_STDOUT_PIPE, + &err, + ); + try ffi.handleGError(&err); + + const communicate_data = try c_allocator.create(StreamlinkCommunicateData); + communicate_data.* = StreamlinkCommunicateData{ + .dialog = dialog, + .text_buf = text_buf, + .window = window, + }; + + c.g_subprocess_communicate_async( + streamlink_subproc, + null, + null, + @ptrCast(c.GAsyncReadyCallback, streamlinkCommunicateCb), + communicate_data, + ); + + if (chatty) { + if (@atomicLoad(bool, &chatty_alive, .Unordered)) { + std.log.warn("Chatty is already running, not starting again.", .{}); + return; + } + + var chatty_arena = std.heap.ArenaAllocator.init(c_allocator); + const channel_d = try chatty_arena.allocator().dupe(u8, channel); + const chatty_argv = [_][]const u8{ "chatty", "-connect", "-channel", channel_d }; + const chatty_argv_dup = try chatty_arena.allocator().dupe([]const u8, &chatty_argv); + var chatty_child = std.ChildProcess.init( + chatty_argv_dup, + c_allocator, + ); + + const thread = try std.Thread.spawn( + .{}, + chattyThread, + .{ chatty_child, chatty_arena }, + ); + thread.detach(); + } +} + +fn chattyThread(child: std.ChildProcess, arena: std.heap.ArenaAllocator) !void { + @atomicStore(bool, &chatty_alive, true, .Unordered); + defer @atomicStore(bool, &chatty_alive, false, .Unordered); + var ch = child; + defer arena.deinit(); + _ = try ch.spawnAndWait(); +} + +const StreamlinkCommunicateData = struct { + dialog: *c.GtkWidget, + text_buf: *c.GtkTextBuffer, + window: *c.GtkWindow, +}; + +fn streamlinkCommunicateCb( + source_object: *c.GObject, + res: *c.GAsyncResult, + data: *StreamlinkCommunicateData, +) void { + defer c_allocator.destroy(data); + + var err: ?*c.GError = null; + var stdout: ?*c.GBytes = null; + _ = c.g_subprocess_communicate_finish( + @ptrCast(*c.GSubprocess, source_object), + res, + &stdout, + null, + &err, + ); + ffi.handleGError(&err) catch { + std.log.err("Failed to communicate to streamlink child!", .{}); + c.gtk_window_close(data.window); + return; + }; + defer c.g_bytes_unref(stdout); + + if (c.g_subprocess_get_exit_status(@ptrCast(*c.GSubprocess, source_object)) == 0) { + c.gtk_window_close(data.window); + return; + } + + var len: usize = 0; + const stdout_data = @ptrCast([*c]const u8, c.g_bytes_get_data(stdout, &len)); + + c.gtk_text_buffer_set_text(data.text_buf, stdout_data, @intCast(c_int, len)); + c.gtk_widget_show(data.dialog); } diff --git a/scripts/playtwitch/src/main.zig b/scripts/playtwitch/src/main.zig index de34a82..082354a 100644 --- a/scripts/playtwitch/src/main.zig +++ b/scripts/playtwitch/src/main.zig @@ -12,7 +12,6 @@ pub fn main() !u8 { defer udata_arena.deinit(); var state = gui.GuiState{ - .alloc = std.heap.c_allocator, .udata_arena = udata_arena.allocator(), }; @@ -27,51 +26,5 @@ pub fn main() !u8 { @ptrCast([*c][*c]u8, std.os.argv.ptr), ); - if (state.streamlink_child) |*sl_child| { - try runChildren(sl_child, if (state.chatty_child) |*ch| ch else null); - } - return @intCast(u8, status); } - -fn runChildren(sl_child: *std.ChildProcess, chatty_child: ?*std.ChildProcess) !void { - var sl_alive = true; - var thread: ?std.Thread = null; - if (chatty_child) |chatty| { - thread = try std.Thread.spawn( - .{}, - waitAndRunChatty, - .{ chatty, &sl_alive }, - ); - } - - sl_child.stdout_behavior = .Pipe; - try sl_child.spawn(); - const output = try sl_child.stdout.?.readToEndAlloc( - std.heap.c_allocator, - std.math.maxInt(usize), - ); - defer std.heap.c_allocator.free(output); - const term = try sl_child.wait(); - if (term == .Exited and term.Exited != 0) { - @atomicStore(bool, &sl_alive, false, .Unordered); - std.log.err("Streamlink died:\n{s}", .{output}); - gui.showStreamlinkErrorDialog(std.mem.trimRight(u8, output, "\n\t ")); - if (thread) |*t| { - t.detach(); - } - } else { - if (thread) |*t| { - t.join(); - } - } -} - -// This function first waits a while, then checks if streamlink is still alive -// and then runs chatty. -fn waitAndRunChatty(chatty: *std.ChildProcess, sl_alive: *bool) !void { - std.time.sleep(5 * std.time.ns_per_s); - if (@atomicLoad(bool, sl_alive, .Unordered)) { - _ = try chatty.spawnAndWait(); - } -}