improve playtwitch

This commit is contained in:
LordMZTE 2022-08-09 12:26:33 +02:00
parent c13ed82cee
commit 2dd33e4ea6
Signed by: LordMZTE
GPG key ID: B64802DC33A64FF6
3 changed files with 175 additions and 113 deletions

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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();
}
}