From 0331d8d7450d28e7b6f3ccd78cccd40177eee984 Mon Sep 17 00:00:00 2001 From: gpucce Date: Sat, 22 Jan 2022 12:40:05 +0100 Subject: [PATCH 01/11] move viewer --- Project.toml | 5 +- src/Javis.jl | 8 +- src/javis_viewer.jl | 359 -------------------------------------- src/structs/Livestream.jl | 19 -- test/runtests.jl | 3 - test/viewer.jl | 109 ------------ 6 files changed, 2 insertions(+), 501 deletions(-) delete mode 100644 src/javis_viewer.jl delete mode 100644 src/structs/Livestream.jl delete mode 100644 test/viewer.jl diff --git a/Project.toml b/Project.toml index 2c5fd0005..da30a7716 100644 --- a/Project.toml +++ b/Project.toml @@ -7,13 +7,12 @@ version = "0.7.2" Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" -Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" -GtkReactive = "27996c0f-39cd-5cc1-a27a-05f136f946b6" Hungarian = "e91730f6-4275-51fb-a7a0-7064cfbd3b39" ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" Interact = "c601a237-2ae4-5e1e-952c-7a85b0c7eef1" +JavisNB = "92afb270-2599-44f6-96a1-44c6efb1daf1" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" LightXML = "9c8b4983-aa76-5018-a973-4c85ecc9e179" Luxor = "ae8d54c2-7ccd-5906-9d76-62fc9837b5bc" @@ -26,8 +25,6 @@ VideoIO = "d6d074c3-1acf-5d4c-9a43-ef38773959a2" Animations = "0.4" Cairo = "1" FFMPEG = "0.3, 0.4" -Gtk = "1.1" -GtkReactive = "1.0.3" Hungarian = "0.6" ImageIO = "0.4, 0.5, 0.6" ImageMagick = "1.1" diff --git a/src/Javis.jl b/src/Javis.jl index ffe095a60..73e2bef51 100644 --- a/src/Javis.jl +++ b/src/Javis.jl @@ -3,8 +3,6 @@ module Javis using Animations import Cairo: CairoImageSurface, image using FFMPEG -using Gtk -using GtkReactive using Hungarian using Images import Interact @@ -102,7 +100,6 @@ include("backgrounds.jl") include("svg2luxor.jl") include("morphs.jl") include("action_animations.jl") -include("javis_viewer.jl") include("latex.jl") include("object_values.jl") @@ -248,7 +245,7 @@ function render( framerate = 30, pathname = "javis_$(randstring(7)).gif", liveview = false, - streamconfig::Union{StreamConfig,Nothing} = nothing, + # streamconfig::Union{StreamConfig,Nothing} = nothing, tempdirectory = "", ffmpeg_loglevel = "panic", rescale_factor = 1.0, @@ -357,9 +354,6 @@ function render( @error "Currently, only gif and mp4 creation is supported. Not a $ext." end - # check if livestream is used and livestream if that's the case - _livestream(streamconfig, framerate, video.width, video.height, pathname) - # clear all CURRENT_* constants to not accidentally use a previous video when creating a new one empty_CURRENT_constants() diff --git a/src/javis_viewer.jl b/src/javis_viewer.jl deleted file mode 100644 index 55b0c276c..000000000 --- a/src/javis_viewer.jl +++ /dev/null @@ -1,359 +0,0 @@ -include("structs/Livestream.jl") - -""" - _draw_image(video::Video, objects::Vector, frame::Int, canvas::Gtk.Canvas, - img_dims::Vector) - -Internal function to create an image that is drawn on a Gtk Canvas. -""" -function _draw_image( - video::Video, - objects::Vector, - frame::Int, - canvas::Gtk.Canvas, - img_dims::Vector, -) - @guarded draw(canvas) do widget - # Gets a specific frame from graphic; transposed due to returned matrix - frame_mat = transpose(get_javis_frame(video, objects, frame; layers = video.layers)) - - # Gets the correct Canvas context to draw on - context = getgc(canvas) - - # Uses Cairo to draw on Gtk canvas context - image(context, CairoImageSurface(frame_mat), 0, 0, img_dims[1], img_dims[2]) - end -end - -""" - _increment(video::Video, widgets::Vector, objects::Vector, dims::Vector, - canvas::Gtk.Canvas, frames::Int, layers=Vector) - -Increments a given value and returns the associated frame. -""" -function _increment( - video::Video, - widgets::Vector, - objects::Vector, - dims::Vector, - canvas::Gtk.Canvas, - frames::Int, -) - # Get current frame from textbox as an Int value - curr_frame = parse(Int, get_gtk_property(widgets[2], :text, String)) - if frames > curr_frame - # `widgets[1]` represents the GtkReactive slider widget - push!(widgets[1], curr_frame + 1) - _draw_image(video, objects, curr_frame + 1, canvas, dims) - else - # `widgets[2]` represents the GtkReactive textboxwidget - push!(widgets[2], 1) # Sets the first frame shown to one - _draw_image(video, objects, 1, canvas, dims) - end -end - -""" - _decrement(video::Video, widgets::Vector, objects::Vector, dims::Vector, - canvas::Gtk.Canvas, frames::Int, layers::Vector) - -Decrements a given value and returns the associated frame. -""" -function _decrement( - video::Video, - widgets::Vector, - objects::Vector, - dims::Vector, - canvas::Gtk.Canvas, - frames::Int, -) - # Get current frame from textbox as an Int value - curr_frame = parse(Int, get_gtk_property(widgets[2], :text, String)) - if curr_frame > 1 - # `widgets[1]` represents the GtkReactive slider widget - push!(widgets[1], curr_frame - 1) - _draw_image(video, objects, curr_frame - 1, canvas, dims) - else - # `widgets[2]` represents the GtkReactive textboxwidget - push!(widgets[2], frames) # Sets the first frame shown to one - _draw_image(video, objects, frames, canvas, dims) - end -end - -""" - _javis_viewer(video::Video, frames::Int, object_list::Vector, show::Bool) - -Internal Javis Viewer built on Gtk that is called for live previewing. -""" -function _javis_viewer( - video::Video, - total_frames::Int, - object_list::Vector, - show::Bool = true, -) - ##################################################################### - # VIEWER WINDOW AND CONFIGURATION - ##################################################################### - - # Determine frame size of animation - frame_dims = [video.width, video.height] - - # Creates a GTK window for drawing; sized based on frame size - win = GtkWindow("Javis Viewer", frame_dims[1], frame_dims[2]) - - # Sets border size of window - set_gtk_property!(win, :border_width, 20) - - ##################################################################### - # DISPLAY WIDGETS - ##################################################################### - - # Create GtkScale internal widget - _slide = GtkScale(false, 1:total_frames) - - # Create GtkReactive slider widget - slide = slider(1:total_frames, value = 1, widget = _slide) - - #= - # - # NOTE: We must provide a named GtkScale widget named `_slide` to the - # GtkReactive `slider` widget so as to perform asynchronous calls - # via signal_connect. Otherwise, we will be unable to update the - # widget that is automatically created by the slider object. - # - # It should be stated that a `slider` object is essentially a - # GtkScale widget coupled with a Reactive object. - # - =# - - # Create a textbox - tbox = GtkReactive.textbox(Int; signal = signal(slide)) - - # Button for going forward through animation - forward = GtkButton("==>") - - # Button for going backward through animation - backward = GtkButton("<==") - - #= - TODO: Enable widgets of window to dynamically resize based on user changing the size of a window. - I think I can use the `configure-event` signal in GTK3 documentation - (link: https://developer.gnome.org/gtk3/stable/GtkWidget.html#GtkWidget-configure-event). - From there, I can then make a `signal_connect` set-up where I update `set_gtk_property!()` - of the windows accordingly using `:width_request` and `height_request`. - =# - - ##################################################################### - # VIEWER CANVAS AND GRID CONFIGURATION - ##################################################################### - - # Gtk Canvas object upon which to draw image; sized via frame size - canvas = Gtk.Canvas(frame_dims[1], frame_dims[2]) - - # Grid to allocate widgets - grid = Gtk.Grid() - - # Allocate the widgets in a 3x3 grid - grid[1:3, 1] = canvas - grid[1:3, 2] = slide - grid[1, 3] = backward - grid[2, 3] = tbox - grid[3, 3] = forward - - # Center all widgets vertically in grid - set_gtk_property!(grid, :valign, 3) - - # Center all widgets horizontally in grid - set_gtk_property!(grid, :halign, 3) - - # Adds grid to previously defined window - push!(win, grid) - - ##################################################################### - # DISPLAY FIRST FRAME - ##################################################################### - - _draw_image(video, object_list, 1, canvas, frame_dims) - - ##################################################################### - # SIGNAL CONNECTION FUNCTIONS - ##################################################################### - - # When the slider is changed, update currently viewed frame - signal_connect(_slide, "value-changed") do widget - # Collects GtkScale as an adjustable bounded value object - bound_slide = Gtk.GAccessor.adjustment(_slide) - - # Get frame number from bounded value object as Int - slide_val = Gtk.get_gtk_property(bound_slide, "value", Int) - - _draw_image(video, object_list, slide_val, canvas, frame_dims) - end - - # When the `Enter` key is pressed, update the frame - signal_connect(win, "key-press-event") do widget, event - if event.keyval == 65293 - # Get current frame from textbox as an Int value - curr_frame = parse(Int, get_gtk_property(tbox, :text, String)) - curr_frame = clamp(curr_frame, 1, total_frames) - _draw_image(video, object_list, curr_frame, canvas, frame_dims) - end - end - - # When the `forward` button is clicked, increment current frame number - # If at final frame, wrap viewer to first frame - signal_connect(forward, "clicked") do widget - _increment(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) - end - - # When the `Right Arrow` key is pressed, increment current frame number - # If at final frame, wrap viewer to first frame - signal_connect(win, "key-press-event") do widget, event - if event.keyval == 65363 - _increment(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) - end - end - - # When the `backward` button is clicked, decrement the current frame number - # If at first frame, wrap viewer to last frame - signal_connect(backward, "clicked") do widget - _decrement(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) - end - - # When the `Left Arrow` key is pressed, decrement current frame number - # If at first frame, wrap viewer to last frame - signal_connect(win, "key-press-event") do widget, event - if event.keyval == 65361 - _decrement(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) - end - end - - ##################################################################### - - if show - # Display image viewer - Gtk.showall(win) - else - return win, frame_dims, slide, tbox, canvas, object_list, total_frames, video - end -end - -""" - setup_stream(livestreamto=:local; protocol="udp", address="0.0.0.0", port=14015, twitch_key="") - -Sets up the livestream configuration. -**NOTE:** Twitch not fully implemented, do not use. -""" -function setup_stream( - livestreamto::Symbol = :local; - protocol::String = "udp", - address::String = "0.0.0.0", - port::Int = 14015, - twitch_key::String = "", -) - StreamConfig(livestreamto, protocol, address, port, twitch_key) -end - -""" - cancel_stream() - -Sends a `SIGKILL` signal to the livestreaming process. Though used internally, it can be used stop streaming. -However this method is not guaranted to end the stream on the client side. -""" -function cancel_stream() - #todo explore better ways of searching and killing processes - - # kill the ffmpeg process - # ps aux | grep ffmpeg | grep stream_loop | awk '{print $2}' | xargs kill -9 - try - println("Checking for existing stream....") - run( - pipeline( - `ps aux`, - pipeline(`grep ffmpeg`, pipeline(`grep stream_loop`, `awk '{print $2}'`)), - ), - ) - catch - return @warn "Not Streaming Anything Currently" - end - - run( - pipeline( - `ps aux`, - pipeline( - `grep ffmpeg`, - pipeline(`grep stream_loop`, pipeline(`awk '{print $2}'`, `xargs kill -9`)), - ), - ), - ) - return "Livestream Cancelled!" -end - -""" - _livestream(streamconfig, framerate, width, height, pathname) - -Internal method for livestreaming -""" -function _livestream( - streamconfig::StreamConfig, - framerate::Int, - width::Int, - height::Int, - pathname::String, -) - cancel_stream() - - livestreamto = streamconfig.livestreamto - twitch_key = streamconfig.twitch_key - - if livestreamto == :twitch && isempty(twitch_key) - return error("Please enter your twitch stream key") - end - - command = [ - "-stream_loop", # loop the stream -1 times i.e. indefinitely - "-1", - "-r", # frames per second - "$framerate", - "-an", # Tells FFMPEG not to expect any audio - "-loglevel", # show only ffmpeg errors - "error", - "-re", # read input at native frame rate - "-i", # input file - "$pathname", - ] - - if livestreamto == :twitch - if isempty(twitch_key) - error("Please enter your twitch api key") - end - - # - twitch_cmd = [ - "-f", - "flv", # force the file to flv format - "rtmp://live.twitch.tv/app/$twitch_key", # stream to the twitch platform using rtmp protocol - ] - push!(command, twitch_cmd...) - @info "Livestreaming to Twitch!" - elseif livestreamto == :local - protocol = streamconfig.protocol - address = streamconfig.address - port = streamconfig.port - local_command = ["-f", "mpegts", "$protocol://$address:$port"] # use an mpeg-ts format, and stream to the given address/port using the protocol - push!(command, local_command...) - @info "Livestream Started at $protocol://$address:$port" - end - - # schedule the streaming process and allow it to run asynchronously - schedule(@task begin - ffmpeg_exe(`$command`) - end) -end - -_livestream( - streamconfig::Nothing, - framerate::Int, - width::Int, - height::Int, - pathname::String, -) = return diff --git a/src/structs/Livestream.jl b/src/structs/Livestream.jl deleted file mode 100644 index 0d914e723..000000000 --- a/src/structs/Livestream.jl +++ /dev/null @@ -1,19 +0,0 @@ -""" - StreamConfig - -Holds the conguration for livestream, defaults to `nothing` - -#Fields -- `livestreamto::Symbol` Livestream platform `:local` or `:twitch` -- `protocol::String` The streaming protocol to be used. Defaults to UDP -- `address::String` The IP address for the `:local` stream(ignored in case of `:twitch`) -- `port::Int` The port for the `:local` stream(ignored in case of `:twitch`) -- `twitch_key::String` Twitch stream key for your account -""" -struct StreamConfig - livestreamto::Symbol - protocol::String - address::String - port::Int - twitch_key::String -end diff --git a/test/runtests.jl b/test/runtests.jl index 849f1dfd3..7ca99a304 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -47,9 +47,6 @@ end @testset "Morphing" begin include("morphing.jl") end - @testset "Javis Viewer" begin - include("viewer.jl") - end @testset "Shorthands" begin include("shorthands.jl") end diff --git a/test/viewer.jl b/test/viewer.jl deleted file mode 100644 index d8f3f8b81..000000000 --- a/test/viewer.jl +++ /dev/null @@ -1,109 +0,0 @@ -function ground(args...) - background("white") - sethue("black") -end - -@testset "Javis Viewer" begin - astar(args...; do_action = :stroke) = star(O, 50, 5, 0.5, 0, do_action) - acirc(args...; do_action = :stroke) = circle(Point(100, 100), 50, do_action) - - vid = Video(500, 500) - back = Background(1:100, ground) - star_obj = Object(1:100, astar) - act!(star_obj, Action(morph_to(acirc; do_action = :fill))) - - l1 = @JLayer 20:60 100 100 Point(0, 0) begin - obj = Object((args...) -> circle(O, 25, :fill)) - act!(obj, Action(1:20, appear(:fade))) - end - - render(vid; pathname = "") - - action_list = [back, star_obj] - - viewer_win, frame_dims, r_slide, tbox, canvas, actions, total_frames, video = - Javis._javis_viewer(vid, 100, action_list, false) - visible(viewer_win, false) - - @test get_gtk_property(viewer_win, :title, String) == "Javis Viewer" - - Javis._increment(video, [r_slide, tbox], actions, frame_dims, canvas, total_frames) - sleep(0.1) - curr_frame = Reactive.value(r_slide) - second_frame = Javis.get_javis_frame(video, actions, curr_frame, layers = [l1]) - @test Reactive.value(r_slide) == 2 - - Javis._decrement(video, [r_slide, tbox], actions, frame_dims, canvas, total_frames) - sleep(0.1) - curr_frame = Reactive.value(r_slide) - first_frame = Javis.get_javis_frame(video, actions, curr_frame, layers = [l1]) - @test Reactive.value(r_slide) == 1 - - @test first_frame != second_frame - - Javis._decrement(video, [r_slide, tbox], actions, frame_dims, canvas, total_frames) - sleep(0.1) - curr_frame = Reactive.value(r_slide) - last_frame = Javis.get_javis_frame(video, actions, curr_frame, layers = [l1]) - @test curr_frame == total_frames - - Javis._increment(video, [r_slide, tbox], actions, frame_dims, canvas, total_frames) - sleep(0.1) - curr_frame = Reactive.value(r_slide) - first_frame = Javis.get_javis_frame(video, actions, curr_frame, layers = [l1]) - @test curr_frame == 1 - - @test last_frame != first_frame -end - - -@testset "Livestreaming" begin - astar(args...; do_action = :stroke) = star(O, 50, 5, 0.5, 0, do_action) - acirc(args...; do_action = :stroke) = circle(Point(100, 100), 50, do_action) - - vid = Video(500, 500) - back = Background(1:100, ground) - star_obj = Object(1:100, astar) - act!(star_obj, Action(morph_to(acirc; do_action = :fill))) - - conf_local = setup_stream(:local, address = "0.0.0.0", port = 8081) - @test conf_local isa Javis.StreamConfig - @test conf_local.livestreamto == :local - @test conf_local.protocol == "udp" - @test conf_local.address == "0.0.0.0" - @test conf_local.port == 8081 - - conf_twitch_err = setup_stream(:twitch) - conf_twitch = setup_stream(:twitch, twitch_key = "foo") - @test conf_twitch_err isa Javis.StreamConfig - @test conf_twitch_err.livestreamto == :twitch - @test isempty(conf_twitch_err.twitch_key) - @test conf_twitch.twitch_key == "foo" - - render(vid, pathname = "stream_local.gif", streamconfig = conf_local) - - # errors with macos; a good test to have - # test_local = run(pipeline(`lsof -i -P -n`, `grep ffmpeg`)) - # @test test_local isa Base.ProcessChain - # @test test_local.processes isa Vector{Base.Process} - - cancel_stream() - @test_throws ProcessFailedException run( - pipeline( - `ps aux`, - pipeline(`grep ffmpeg`, pipeline(`grep stream_loop`, `awk '{print $2}'`)), - ), - ) - - vid = Video(500, 500) - back = Background(1:100, ground) - star_obj = Object(1:100, astar) - act!(star_obj, Action(morph_to(acirc; do_action = :fill))) - - @test_throws ErrorException render( - vid, - pathname = "stream_twitch.gif", - streamconfig = conf_twitch_err, - ) - rm("stream_twitch.gif") -end From c314a8d2653aeb319076809b5eea9274d9cb5c69 Mon Sep 17 00:00:00 2001 From: gpucce Date: Sat, 22 Jan 2022 12:48:06 +0100 Subject: [PATCH 02/11] remove JavisNB dep for now --- Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Project.toml b/Project.toml index da30a7716..757987f61 100644 --- a/Project.toml +++ b/Project.toml @@ -12,7 +12,6 @@ ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" Interact = "c601a237-2ae4-5e1e-952c-7a85b0c7eef1" -JavisNB = "92afb270-2599-44f6-96a1-44c6efb1daf1" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" LightXML = "9c8b4983-aa76-5018-a973-4c85ecc9e179" Luxor = "ae8d54c2-7ccd-5906-9d76-62fc9837b5bc" From 6a8953c6078cabdcff2f12a5dcaf97ec1677cc6f Mon Sep 17 00:00:00 2001 From: gpucce Date: Sat, 22 Jan 2022 13:36:13 +0100 Subject: [PATCH 03/11] adapt liveview to JavisViewer --- src/Javis.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Javis.jl b/src/Javis.jl index 73e2bef51..783654a0c 100644 --- a/src/Javis.jl +++ b/src/Javis.jl @@ -264,8 +264,7 @@ function render( return video, length(frames), objects else - _javis_viewer(video, length(frames), objects) - return "Live Preview Started" + return video, length(frames), objects end end @@ -733,7 +732,6 @@ export JBox, JCircle, JEllipse, JLine, JPoly, JRect, JStar, @JShape # custom override of luxor extensions export setline, setopacity, fontsize, get_fontsize, scale, text -export setup_stream, cancel_stream # scales export scale_linear, @scale_layer From 27f7699241cf49b155d2a6666616feb3beae6d71 Mon Sep 17 00:00:00 2001 From: gpucce Date: Fri, 25 Feb 2022 19:52:58 +0100 Subject: [PATCH 04/11] start rebuilding liveviewer --- src/javis_viewer.jl | 359 ++++++++++++++++++++++++++++++++++++++ src/structs/Livestream.jl | 19 ++ test/livestream.jl | 55 ++++++ 3 files changed, 433 insertions(+) create mode 100644 src/javis_viewer.jl create mode 100644 src/structs/Livestream.jl create mode 100644 test/livestream.jl diff --git a/src/javis_viewer.jl b/src/javis_viewer.jl new file mode 100644 index 000000000..55b0c276c --- /dev/null +++ b/src/javis_viewer.jl @@ -0,0 +1,359 @@ +include("structs/Livestream.jl") + +""" + _draw_image(video::Video, objects::Vector, frame::Int, canvas::Gtk.Canvas, + img_dims::Vector) + +Internal function to create an image that is drawn on a Gtk Canvas. +""" +function _draw_image( + video::Video, + objects::Vector, + frame::Int, + canvas::Gtk.Canvas, + img_dims::Vector, +) + @guarded draw(canvas) do widget + # Gets a specific frame from graphic; transposed due to returned matrix + frame_mat = transpose(get_javis_frame(video, objects, frame; layers = video.layers)) + + # Gets the correct Canvas context to draw on + context = getgc(canvas) + + # Uses Cairo to draw on Gtk canvas context + image(context, CairoImageSurface(frame_mat), 0, 0, img_dims[1], img_dims[2]) + end +end + +""" + _increment(video::Video, widgets::Vector, objects::Vector, dims::Vector, + canvas::Gtk.Canvas, frames::Int, layers=Vector) + +Increments a given value and returns the associated frame. +""" +function _increment( + video::Video, + widgets::Vector, + objects::Vector, + dims::Vector, + canvas::Gtk.Canvas, + frames::Int, +) + # Get current frame from textbox as an Int value + curr_frame = parse(Int, get_gtk_property(widgets[2], :text, String)) + if frames > curr_frame + # `widgets[1]` represents the GtkReactive slider widget + push!(widgets[1], curr_frame + 1) + _draw_image(video, objects, curr_frame + 1, canvas, dims) + else + # `widgets[2]` represents the GtkReactive textboxwidget + push!(widgets[2], 1) # Sets the first frame shown to one + _draw_image(video, objects, 1, canvas, dims) + end +end + +""" + _decrement(video::Video, widgets::Vector, objects::Vector, dims::Vector, + canvas::Gtk.Canvas, frames::Int, layers::Vector) + +Decrements a given value and returns the associated frame. +""" +function _decrement( + video::Video, + widgets::Vector, + objects::Vector, + dims::Vector, + canvas::Gtk.Canvas, + frames::Int, +) + # Get current frame from textbox as an Int value + curr_frame = parse(Int, get_gtk_property(widgets[2], :text, String)) + if curr_frame > 1 + # `widgets[1]` represents the GtkReactive slider widget + push!(widgets[1], curr_frame - 1) + _draw_image(video, objects, curr_frame - 1, canvas, dims) + else + # `widgets[2]` represents the GtkReactive textboxwidget + push!(widgets[2], frames) # Sets the first frame shown to one + _draw_image(video, objects, frames, canvas, dims) + end +end + +""" + _javis_viewer(video::Video, frames::Int, object_list::Vector, show::Bool) + +Internal Javis Viewer built on Gtk that is called for live previewing. +""" +function _javis_viewer( + video::Video, + total_frames::Int, + object_list::Vector, + show::Bool = true, +) + ##################################################################### + # VIEWER WINDOW AND CONFIGURATION + ##################################################################### + + # Determine frame size of animation + frame_dims = [video.width, video.height] + + # Creates a GTK window for drawing; sized based on frame size + win = GtkWindow("Javis Viewer", frame_dims[1], frame_dims[2]) + + # Sets border size of window + set_gtk_property!(win, :border_width, 20) + + ##################################################################### + # DISPLAY WIDGETS + ##################################################################### + + # Create GtkScale internal widget + _slide = GtkScale(false, 1:total_frames) + + # Create GtkReactive slider widget + slide = slider(1:total_frames, value = 1, widget = _slide) + + #= + # + # NOTE: We must provide a named GtkScale widget named `_slide` to the + # GtkReactive `slider` widget so as to perform asynchronous calls + # via signal_connect. Otherwise, we will be unable to update the + # widget that is automatically created by the slider object. + # + # It should be stated that a `slider` object is essentially a + # GtkScale widget coupled with a Reactive object. + # + =# + + # Create a textbox + tbox = GtkReactive.textbox(Int; signal = signal(slide)) + + # Button for going forward through animation + forward = GtkButton("==>") + + # Button for going backward through animation + backward = GtkButton("<==") + + #= + TODO: Enable widgets of window to dynamically resize based on user changing the size of a window. + I think I can use the `configure-event` signal in GTK3 documentation + (link: https://developer.gnome.org/gtk3/stable/GtkWidget.html#GtkWidget-configure-event). + From there, I can then make a `signal_connect` set-up where I update `set_gtk_property!()` + of the windows accordingly using `:width_request` and `height_request`. + =# + + ##################################################################### + # VIEWER CANVAS AND GRID CONFIGURATION + ##################################################################### + + # Gtk Canvas object upon which to draw image; sized via frame size + canvas = Gtk.Canvas(frame_dims[1], frame_dims[2]) + + # Grid to allocate widgets + grid = Gtk.Grid() + + # Allocate the widgets in a 3x3 grid + grid[1:3, 1] = canvas + grid[1:3, 2] = slide + grid[1, 3] = backward + grid[2, 3] = tbox + grid[3, 3] = forward + + # Center all widgets vertically in grid + set_gtk_property!(grid, :valign, 3) + + # Center all widgets horizontally in grid + set_gtk_property!(grid, :halign, 3) + + # Adds grid to previously defined window + push!(win, grid) + + ##################################################################### + # DISPLAY FIRST FRAME + ##################################################################### + + _draw_image(video, object_list, 1, canvas, frame_dims) + + ##################################################################### + # SIGNAL CONNECTION FUNCTIONS + ##################################################################### + + # When the slider is changed, update currently viewed frame + signal_connect(_slide, "value-changed") do widget + # Collects GtkScale as an adjustable bounded value object + bound_slide = Gtk.GAccessor.adjustment(_slide) + + # Get frame number from bounded value object as Int + slide_val = Gtk.get_gtk_property(bound_slide, "value", Int) + + _draw_image(video, object_list, slide_val, canvas, frame_dims) + end + + # When the `Enter` key is pressed, update the frame + signal_connect(win, "key-press-event") do widget, event + if event.keyval == 65293 + # Get current frame from textbox as an Int value + curr_frame = parse(Int, get_gtk_property(tbox, :text, String)) + curr_frame = clamp(curr_frame, 1, total_frames) + _draw_image(video, object_list, curr_frame, canvas, frame_dims) + end + end + + # When the `forward` button is clicked, increment current frame number + # If at final frame, wrap viewer to first frame + signal_connect(forward, "clicked") do widget + _increment(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) + end + + # When the `Right Arrow` key is pressed, increment current frame number + # If at final frame, wrap viewer to first frame + signal_connect(win, "key-press-event") do widget, event + if event.keyval == 65363 + _increment(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) + end + end + + # When the `backward` button is clicked, decrement the current frame number + # If at first frame, wrap viewer to last frame + signal_connect(backward, "clicked") do widget + _decrement(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) + end + + # When the `Left Arrow` key is pressed, decrement current frame number + # If at first frame, wrap viewer to last frame + signal_connect(win, "key-press-event") do widget, event + if event.keyval == 65361 + _decrement(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) + end + end + + ##################################################################### + + if show + # Display image viewer + Gtk.showall(win) + else + return win, frame_dims, slide, tbox, canvas, object_list, total_frames, video + end +end + +""" + setup_stream(livestreamto=:local; protocol="udp", address="0.0.0.0", port=14015, twitch_key="") + +Sets up the livestream configuration. +**NOTE:** Twitch not fully implemented, do not use. +""" +function setup_stream( + livestreamto::Symbol = :local; + protocol::String = "udp", + address::String = "0.0.0.0", + port::Int = 14015, + twitch_key::String = "", +) + StreamConfig(livestreamto, protocol, address, port, twitch_key) +end + +""" + cancel_stream() + +Sends a `SIGKILL` signal to the livestreaming process. Though used internally, it can be used stop streaming. +However this method is not guaranted to end the stream on the client side. +""" +function cancel_stream() + #todo explore better ways of searching and killing processes + + # kill the ffmpeg process + # ps aux | grep ffmpeg | grep stream_loop | awk '{print $2}' | xargs kill -9 + try + println("Checking for existing stream....") + run( + pipeline( + `ps aux`, + pipeline(`grep ffmpeg`, pipeline(`grep stream_loop`, `awk '{print $2}'`)), + ), + ) + catch + return @warn "Not Streaming Anything Currently" + end + + run( + pipeline( + `ps aux`, + pipeline( + `grep ffmpeg`, + pipeline(`grep stream_loop`, pipeline(`awk '{print $2}'`, `xargs kill -9`)), + ), + ), + ) + return "Livestream Cancelled!" +end + +""" + _livestream(streamconfig, framerate, width, height, pathname) + +Internal method for livestreaming +""" +function _livestream( + streamconfig::StreamConfig, + framerate::Int, + width::Int, + height::Int, + pathname::String, +) + cancel_stream() + + livestreamto = streamconfig.livestreamto + twitch_key = streamconfig.twitch_key + + if livestreamto == :twitch && isempty(twitch_key) + return error("Please enter your twitch stream key") + end + + command = [ + "-stream_loop", # loop the stream -1 times i.e. indefinitely + "-1", + "-r", # frames per second + "$framerate", + "-an", # Tells FFMPEG not to expect any audio + "-loglevel", # show only ffmpeg errors + "error", + "-re", # read input at native frame rate + "-i", # input file + "$pathname", + ] + + if livestreamto == :twitch + if isempty(twitch_key) + error("Please enter your twitch api key") + end + + # + twitch_cmd = [ + "-f", + "flv", # force the file to flv format + "rtmp://live.twitch.tv/app/$twitch_key", # stream to the twitch platform using rtmp protocol + ] + push!(command, twitch_cmd...) + @info "Livestreaming to Twitch!" + elseif livestreamto == :local + protocol = streamconfig.protocol + address = streamconfig.address + port = streamconfig.port + local_command = ["-f", "mpegts", "$protocol://$address:$port"] # use an mpeg-ts format, and stream to the given address/port using the protocol + push!(command, local_command...) + @info "Livestream Started at $protocol://$address:$port" + end + + # schedule the streaming process and allow it to run asynchronously + schedule(@task begin + ffmpeg_exe(`$command`) + end) +end + +_livestream( + streamconfig::Nothing, + framerate::Int, + width::Int, + height::Int, + pathname::String, +) = return diff --git a/src/structs/Livestream.jl b/src/structs/Livestream.jl new file mode 100644 index 000000000..0d914e723 --- /dev/null +++ b/src/structs/Livestream.jl @@ -0,0 +1,19 @@ +""" + StreamConfig + +Holds the conguration for livestream, defaults to `nothing` + +#Fields +- `livestreamto::Symbol` Livestream platform `:local` or `:twitch` +- `protocol::String` The streaming protocol to be used. Defaults to UDP +- `address::String` The IP address for the `:local` stream(ignored in case of `:twitch`) +- `port::Int` The port for the `:local` stream(ignored in case of `:twitch`) +- `twitch_key::String` Twitch stream key for your account +""" +struct StreamConfig + livestreamto::Symbol + protocol::String + address::String + port::Int + twitch_key::String +end diff --git a/test/livestream.jl b/test/livestream.jl new file mode 100644 index 000000000..23d3c87ee --- /dev/null +++ b/test/livestream.jl @@ -0,0 +1,55 @@ +function ground(args...) + background("white") + sethue("black") +end + +@testset "Livestreaming" begin + astar(args...; do_action = :stroke) = star(O, 50, 5, 0.5, 0, do_action) + acirc(args...; do_action = :stroke) = circle(Point(100, 100), 50, do_action) + + vid = Video(500, 500) + back = Background(1:100, ground) + star_obj = Object(1:100, astar) + act!(star_obj, Action(morph_to(acirc; do_action = :fill))) + + conf_local = setup_stream(:local, address = "0.0.0.0", port = 8081) + @test conf_local isa Javis.StreamConfig + @test conf_local.livestreamto == :local + @test conf_local.protocol == "udp" + @test conf_local.address == "0.0.0.0" + @test conf_local.port == 8081 + + conf_twitch_err = setup_stream(:twitch) + conf_twitch = setup_stream(:twitch, twitch_key = "foo") + @test conf_twitch_err isa Javis.StreamConfig + @test conf_twitch_err.livestreamto == :twitch + @test isempty(conf_twitch_err.twitch_key) + @test conf_twitch.twitch_key == "foo" + + render(vid, pathname = "stream_local.gif", streamconfig = conf_local) + + # errors with macos; a good test to have + # test_local = run(pipeline(`lsof -i -P -n`, `grep ffmpeg`)) + # @test test_local isa Base.ProcessChain + # @test test_local.processes isa Vector{Base.Process} + + cancel_stream() + @test_throws ProcessFailedException run( + pipeline( + `ps aux`, + pipeline(`grep ffmpeg`, pipeline(`grep stream_loop`, `awk '{print $2}'`)), + ), + ) + + vid = Video(500, 500) + back = Background(1:100, ground) + star_obj = Object(1:100, astar) + act!(star_obj, Action(morph_to(acirc; do_action = :fill))) + + @test_throws ErrorException stream( + vid, + pathname = "stream_twitch.gif", + streamconfig = conf_twitch_err, + ) + rm("stream_twitch.gif") +end \ No newline at end of file From f4e0eb3b17fd87d0cdfceb178f9bfc7cbb454404 Mon Sep 17 00:00:00 2001 From: gpucce Date: Fri, 25 Feb 2022 20:00:08 +0100 Subject: [PATCH 05/11] moving on --- Project.toml | 2 ++ src/Javis.jl | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 223e71f89..407e3a17c 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,8 @@ version = "0.8.0" Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" +Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" +GtkReactive = "27996c0f-39cd-5cc1-a27a-05f136f946b6" Hungarian = "e91730f6-4275-51fb-a7a0-7064cfbd3b39" ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" diff --git a/src/Javis.jl b/src/Javis.jl index 783654a0c..c64166a59 100644 --- a/src/Javis.jl +++ b/src/Javis.jl @@ -3,6 +3,8 @@ module Javis using Animations import Cairo: CairoImageSurface, image using FFMPEG +using Gtk +using GtkReactive using Hungarian using Images import Interact @@ -100,9 +102,13 @@ include("backgrounds.jl") include("svg2luxor.jl") include("morphs.jl") include("action_animations.jl") +include("javis_viewer.jl") include("latex.jl") include("object_values.jl") +export stream +export setup_stream, cancel_stream + """ projection(p::Point, l::Line) @@ -245,7 +251,7 @@ function render( framerate = 30, pathname = "javis_$(randstring(7)).gif", liveview = false, - # streamconfig::Union{StreamConfig,Nothing} = nothing, + streamconfig::Union{StreamConfig,Nothing} = nothing, tempdirectory = "", ffmpeg_loglevel = "panic", rescale_factor = 1.0, From d5eb8bf39d27e008a6ed03c3c57812099f331c40 Mon Sep 17 00:00:00 2001 From: gpucce Date: Sat, 26 Feb 2022 11:03:15 +0100 Subject: [PATCH 06/11] minor switch --- test/livestream.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/livestream.jl b/test/livestream.jl index 23d3c87ee..95d467bd2 100644 --- a/test/livestream.jl +++ b/test/livestream.jl @@ -46,7 +46,7 @@ end star_obj = Object(1:100, astar) act!(star_obj, Action(morph_to(acirc; do_action = :fill))) - @test_throws ErrorException stream( + @test_throws ErrorException render( vid, pathname = "stream_twitch.gif", streamconfig = conf_twitch_err, From 606321133dce92a5b3f60e55aa38ce03c30133ac Mon Sep 17 00:00:00 2001 From: gpucce Date: Sat, 26 Feb 2022 21:48:50 +0100 Subject: [PATCH 07/11] mote steps --- test/livestream.jl | 2 +- test/runtests.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/livestream.jl b/test/livestream.jl index 23d3c87ee..95d467bd2 100644 --- a/test/livestream.jl +++ b/test/livestream.jl @@ -46,7 +46,7 @@ end star_obj = Object(1:100, astar) act!(star_obj, Action(morph_to(acirc; do_action = :fill))) - @test_throws ErrorException stream( + @test_throws ErrorException render( vid, pathname = "stream_twitch.gif", streamconfig = conf_twitch_err, diff --git a/test/runtests.jl b/test/runtests.jl index d9e2eaf52..db3a75372 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -56,7 +56,7 @@ end @testset "Postprocessing" begin include("postprocessing.jl") end - @testset "Javis Viewer" begin - include("viewer.jl") + @testset "Javis LiveStream" begin + include("livestream.jl") end end From 45fe1f8b163d255e14b38941c03ab8701db40ad1 Mon Sep 17 00:00:00 2001 From: gpucce Date: Sat, 26 Feb 2022 22:03:59 +0100 Subject: [PATCH 08/11] small change --- src/Javis.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Javis.jl b/src/Javis.jl index c64166a59..44650f252 100644 --- a/src/Javis.jl +++ b/src/Javis.jl @@ -106,7 +106,7 @@ include("javis_viewer.jl") include("latex.jl") include("object_values.jl") -export stream + export setup_stream, cancel_stream """ From 90f0554d7b5672f2ed24a02fae08bd23220a7004 Mon Sep 17 00:00:00 2001 From: gpucce Date: Sat, 26 Feb 2022 22:07:44 +0100 Subject: [PATCH 09/11] readd livestream --- src/Javis.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Javis.jl b/src/Javis.jl index 44650f252..86e6352dc 100644 --- a/src/Javis.jl +++ b/src/Javis.jl @@ -359,6 +359,9 @@ function render( @error "Currently, only gif and mp4 creation is supported. Not a $ext." end + # check if livestream is used and livestream if that's the case + _livestream(streamconfig, framerate, video.width, video.height, pathname) + # clear all CURRENT_* constants to not accidentally use a previous video when creating a new one empty_CURRENT_constants() From 65e1797f78f82b778836217f47ad97f7537d7697 Mon Sep 17 00:00:00 2001 From: gpucce Date: Sun, 27 Feb 2022 00:17:12 +0100 Subject: [PATCH 10/11] format --- test/livestream.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/livestream.jl b/test/livestream.jl index 95d467bd2..97c36c699 100644 --- a/test/livestream.jl +++ b/test/livestream.jl @@ -52,4 +52,4 @@ end streamconfig = conf_twitch_err, ) rm("stream_twitch.gif") -end \ No newline at end of file +end From 0df92a1d40678d293652a96b0a7bd6ee45640cf7 Mon Sep 17 00:00:00 2001 From: gpucce Date: Sun, 27 Feb 2022 01:19:59 +0100 Subject: [PATCH 11/11] move more stuff away --- src/javis_viewer.jl | 237 -------------------------------------------- 1 file changed, 237 deletions(-) diff --git a/src/javis_viewer.jl b/src/javis_viewer.jl index 55b0c276c..0d380c896 100644 --- a/src/javis_viewer.jl +++ b/src/javis_viewer.jl @@ -1,242 +1,5 @@ include("structs/Livestream.jl") -""" - _draw_image(video::Video, objects::Vector, frame::Int, canvas::Gtk.Canvas, - img_dims::Vector) - -Internal function to create an image that is drawn on a Gtk Canvas. -""" -function _draw_image( - video::Video, - objects::Vector, - frame::Int, - canvas::Gtk.Canvas, - img_dims::Vector, -) - @guarded draw(canvas) do widget - # Gets a specific frame from graphic; transposed due to returned matrix - frame_mat = transpose(get_javis_frame(video, objects, frame; layers = video.layers)) - - # Gets the correct Canvas context to draw on - context = getgc(canvas) - - # Uses Cairo to draw on Gtk canvas context - image(context, CairoImageSurface(frame_mat), 0, 0, img_dims[1], img_dims[2]) - end -end - -""" - _increment(video::Video, widgets::Vector, objects::Vector, dims::Vector, - canvas::Gtk.Canvas, frames::Int, layers=Vector) - -Increments a given value and returns the associated frame. -""" -function _increment( - video::Video, - widgets::Vector, - objects::Vector, - dims::Vector, - canvas::Gtk.Canvas, - frames::Int, -) - # Get current frame from textbox as an Int value - curr_frame = parse(Int, get_gtk_property(widgets[2], :text, String)) - if frames > curr_frame - # `widgets[1]` represents the GtkReactive slider widget - push!(widgets[1], curr_frame + 1) - _draw_image(video, objects, curr_frame + 1, canvas, dims) - else - # `widgets[2]` represents the GtkReactive textboxwidget - push!(widgets[2], 1) # Sets the first frame shown to one - _draw_image(video, objects, 1, canvas, dims) - end -end - -""" - _decrement(video::Video, widgets::Vector, objects::Vector, dims::Vector, - canvas::Gtk.Canvas, frames::Int, layers::Vector) - -Decrements a given value and returns the associated frame. -""" -function _decrement( - video::Video, - widgets::Vector, - objects::Vector, - dims::Vector, - canvas::Gtk.Canvas, - frames::Int, -) - # Get current frame from textbox as an Int value - curr_frame = parse(Int, get_gtk_property(widgets[2], :text, String)) - if curr_frame > 1 - # `widgets[1]` represents the GtkReactive slider widget - push!(widgets[1], curr_frame - 1) - _draw_image(video, objects, curr_frame - 1, canvas, dims) - else - # `widgets[2]` represents the GtkReactive textboxwidget - push!(widgets[2], frames) # Sets the first frame shown to one - _draw_image(video, objects, frames, canvas, dims) - end -end - -""" - _javis_viewer(video::Video, frames::Int, object_list::Vector, show::Bool) - -Internal Javis Viewer built on Gtk that is called for live previewing. -""" -function _javis_viewer( - video::Video, - total_frames::Int, - object_list::Vector, - show::Bool = true, -) - ##################################################################### - # VIEWER WINDOW AND CONFIGURATION - ##################################################################### - - # Determine frame size of animation - frame_dims = [video.width, video.height] - - # Creates a GTK window for drawing; sized based on frame size - win = GtkWindow("Javis Viewer", frame_dims[1], frame_dims[2]) - - # Sets border size of window - set_gtk_property!(win, :border_width, 20) - - ##################################################################### - # DISPLAY WIDGETS - ##################################################################### - - # Create GtkScale internal widget - _slide = GtkScale(false, 1:total_frames) - - # Create GtkReactive slider widget - slide = slider(1:total_frames, value = 1, widget = _slide) - - #= - # - # NOTE: We must provide a named GtkScale widget named `_slide` to the - # GtkReactive `slider` widget so as to perform asynchronous calls - # via signal_connect. Otherwise, we will be unable to update the - # widget that is automatically created by the slider object. - # - # It should be stated that a `slider` object is essentially a - # GtkScale widget coupled with a Reactive object. - # - =# - - # Create a textbox - tbox = GtkReactive.textbox(Int; signal = signal(slide)) - - # Button for going forward through animation - forward = GtkButton("==>") - - # Button for going backward through animation - backward = GtkButton("<==") - - #= - TODO: Enable widgets of window to dynamically resize based on user changing the size of a window. - I think I can use the `configure-event` signal in GTK3 documentation - (link: https://developer.gnome.org/gtk3/stable/GtkWidget.html#GtkWidget-configure-event). - From there, I can then make a `signal_connect` set-up where I update `set_gtk_property!()` - of the windows accordingly using `:width_request` and `height_request`. - =# - - ##################################################################### - # VIEWER CANVAS AND GRID CONFIGURATION - ##################################################################### - - # Gtk Canvas object upon which to draw image; sized via frame size - canvas = Gtk.Canvas(frame_dims[1], frame_dims[2]) - - # Grid to allocate widgets - grid = Gtk.Grid() - - # Allocate the widgets in a 3x3 grid - grid[1:3, 1] = canvas - grid[1:3, 2] = slide - grid[1, 3] = backward - grid[2, 3] = tbox - grid[3, 3] = forward - - # Center all widgets vertically in grid - set_gtk_property!(grid, :valign, 3) - - # Center all widgets horizontally in grid - set_gtk_property!(grid, :halign, 3) - - # Adds grid to previously defined window - push!(win, grid) - - ##################################################################### - # DISPLAY FIRST FRAME - ##################################################################### - - _draw_image(video, object_list, 1, canvas, frame_dims) - - ##################################################################### - # SIGNAL CONNECTION FUNCTIONS - ##################################################################### - - # When the slider is changed, update currently viewed frame - signal_connect(_slide, "value-changed") do widget - # Collects GtkScale as an adjustable bounded value object - bound_slide = Gtk.GAccessor.adjustment(_slide) - - # Get frame number from bounded value object as Int - slide_val = Gtk.get_gtk_property(bound_slide, "value", Int) - - _draw_image(video, object_list, slide_val, canvas, frame_dims) - end - - # When the `Enter` key is pressed, update the frame - signal_connect(win, "key-press-event") do widget, event - if event.keyval == 65293 - # Get current frame from textbox as an Int value - curr_frame = parse(Int, get_gtk_property(tbox, :text, String)) - curr_frame = clamp(curr_frame, 1, total_frames) - _draw_image(video, object_list, curr_frame, canvas, frame_dims) - end - end - - # When the `forward` button is clicked, increment current frame number - # If at final frame, wrap viewer to first frame - signal_connect(forward, "clicked") do widget - _increment(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) - end - - # When the `Right Arrow` key is pressed, increment current frame number - # If at final frame, wrap viewer to first frame - signal_connect(win, "key-press-event") do widget, event - if event.keyval == 65363 - _increment(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) - end - end - - # When the `backward` button is clicked, decrement the current frame number - # If at first frame, wrap viewer to last frame - signal_connect(backward, "clicked") do widget - _decrement(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) - end - - # When the `Left Arrow` key is pressed, decrement current frame number - # If at first frame, wrap viewer to last frame - signal_connect(win, "key-press-event") do widget, event - if event.keyval == 65361 - _decrement(video, [slide, tbox], object_list, frame_dims, canvas, total_frames) - end - end - - ##################################################################### - - if show - # Display image viewer - Gtk.showall(win) - else - return win, frame_dims, slide, tbox, canvas, object_list, total_frames, video - end -end - """ setup_stream(livestreamto=:local; protocol="udp", address="0.0.0.0", port=14015, twitch_key="")