From: Daniel Karbach Date: Thu, 21 Nov 2024 18:34:46 +0000 (+0100) Subject: enhanced clip player X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=HEAD;p=ffmpeg-test.git enhanced clip player --- diff --git a/src/app/Application.h b/src/app/Application.h index 1d7d6b6..200b36c 100644 --- a/src/app/Application.h +++ b/src/app/Application.h @@ -62,22 +62,10 @@ public: twitch_conn.Join(config.chat_channel).Listen([this](const twitch::IRCMessage &msg) -> void { HandleTwitch(msg); }); - ws_ctx.HttpsRequest("GET", "alttp.localhorst.tv", "/api/channels?chatting=1").GetPromise().Then([this](ws::HttpsConnection *rsp) -> void { + ws_ctx.HttpsRequest("GET", "alttp.localhorst.tv", "/api/channels?logging=1").GetPromise().Then([this](ws::HttpsConnection *rsp) -> void { InitChannels(*rsp); }); stream.Start(); - - //Media &media_a = state.AddMedia("test.mp4"); - //Clock sync_point_a = stream.GetVideoClock(); - //sync_point_a.Advance(1200); - //media_a.SetSyncPoint(sync_point_a); - //media_a.AddWindow({ 0, 0, 1, 1 }, { 100, 250, 640, 360 }); - - //Media &media_b = state.AddMedia("test.mkv"); - //Clock sync_point_b = stream.GetVideoClock(); - //sync_point_b.Advance(600); - //media_b.SetSyncPoint(sync_point_b); - //media_b.AddWindow({ 0, 0, 1, 1 }, { 600, 50, 640, 360 }); } void Step() { @@ -106,6 +94,10 @@ public: stream.PushAudioFrame(); } + if (state.IRCMessageReady()) { + SendIRCText(state.PopIRCMessage()); + } + state.Clean(); if (enable_realtime && difference > 1000) { @@ -169,7 +161,7 @@ private: if (config.shoutouts) { twitch_conn.Shoutout(config.own_channel_id, channel.twitch_id); } - shout.FetchClip(twitch_conn); + EnqueueClip(id); } } @@ -205,7 +197,7 @@ private: if (channel) { ShoutoutChannel(channel->id); } else { - SendIRCText("Channel nicht gefunden PoroSad"); + state.QueueIRCMessage("Channel nicht gefunden PoroSad"); } } else if (msg.StartsWith("!clip")) { const ChannelInfo *channel = nullptr; @@ -221,7 +213,7 @@ private: if (channel) { EnqueueClip(channel->id); } else { - SendIRCText("Channel nicht gefunden NotLikeThis"); + state.QueueIRCMessage("Channel nicht gefunden NotLikeThis"); } } if (state.HasGame()) { diff --git a/src/app/ChannelInfo.h b/src/app/ChannelInfo.h index e41722a..c782b19 100644 --- a/src/app/ChannelInfo.h +++ b/src/app/ChannelInfo.h @@ -15,6 +15,8 @@ struct ChannelInfo { explicit ChannelInfo(const Json::Value &json) : id(json["id"].asInt()) , title(json["title"].asString()) + , short_name(json["short_name"].asString()) + , stream_link(json["stream_link"].asString()) , twitch_id(json["twitch_id"].asString()) , twitch_chat(json["twitch_chat"].asString()) , twitch_title(json["twitch_title"].asString()) @@ -29,6 +31,8 @@ struct ChannelInfo { throw std::runtime_error("update channel ID mismatch"); } title = json["title"].asString(); + short_name = json["short_name"].asString(); + stream_link = json["stream_link"].asString(); twitch_id = json["twitch_id"].asString(); twitch_chat = json["twitch_chat"].asString(); twitch_title = json["twitch_title"].asString(); @@ -40,6 +44,8 @@ struct ChannelInfo { int id; std::string title; + std::string short_name; + std::string stream_link; std::string twitch_id; std::string twitch_chat; std::string twitch_title; diff --git a/src/app/ClipPlayer.cpp b/src/app/ClipPlayer.cpp index 988207a..ea840b8 100644 --- a/src/app/ClipPlayer.cpp +++ b/src/app/ClipPlayer.cpp @@ -9,8 +9,14 @@ namespace app { void ClipPlayer::LoadRandomClip(const Json::Value &json) { const Json::Value &data = json["data"]; - if (!data.isArray()) return; - if (data.empty()) return; + if (!data.isArray()) { + state.QueueIRCMessage("Ungültiges Datenformat in der Twitch API Response DansGame"); + return; + } + if (data.empty()) { + state.QueueIRCMessage("Channel " + channel.title + " hat keine Clips NotLikeThis"); + return; + } Json::ArrayIndex num = data.size(); std::uniform_int_distribution dist(0, num - 1); Json::ArrayIndex choice = dist(state.GetRNG()); @@ -25,11 +31,15 @@ void ClipPlayer::Start(const Clock &clock) { std::cout << "adding clip " << clip.GetVideoURL() << " at " << clock << std::endl; Media &media = state.AddMedia(clip.GetVideoURL().c_str()); media.SetSyncPoint(clock); - media.AddWindow({ 0, 0, 1, 1 }, { 320, 150, 640, 360 }); + media.AddWindow({ 0, 0, 1, 1 }, screen); media.OnComplete([this](Media *) -> void { done = true; }); + title_layout.SetText(clip.GetTitle()); + channel_layout.SetText(clip.GetBroadcasterName()); + text_dirty = true; } else { + state.QueueIRCMessage("Clip " + clip.GetClipURL() + " hat keine Video URL Sadge"); std::cout << "clip has no video (" << clip.GetVideoURL() << ")" << std::endl; done = true; } diff --git a/src/app/ClipPlayer.h b/src/app/ClipPlayer.h index e201651..6eb9d4b 100644 --- a/src/app/ClipPlayer.h +++ b/src/app/ClipPlayer.h @@ -1,9 +1,13 @@ #ifndef TEST_APP_CLIPPLAYER_H_ #define TEST_APP_CLIPPLAYER_H_ +#include + #include "ChannelInfo.h" #include "Clock.h" #include "../cairo/Context.h" +#include "../gfx/Rectangle.h" +#include "../gfx/Spacing.h" #include "../twitch/Clip.h" #include "../ws/TwitchConnection.h" @@ -17,10 +21,25 @@ public: explicit ClipPlayer(const ChannelInfo &channel, cairo::Context &ctx, State &state) : channel(channel) , state(state) + , title_layout(ctx.CreateLayout()) + , channel_layout(ctx.CreateLayout()) + , text_dirty(true) + , bg_color{ 0.1, 0.1, 0.1 } + , title_color{ 1, 1, 1 } + , channel_color{ 0.392, 0.255, 0.647 } + , progress_color{ 0.8, 0.1, 0.1 } + , padding(10) + , screen{ 400, 50, 640, 360 } + , text_box{ screen.x, screen.y + screen.h, screen.w, 0.0 } + , title_pos(gfx::Position{ text_box.x, text_box.y } + padding.Offset()) + , channel_pos(gfx::Position{ text_box.x, text_box.y } + padding.Offset(0, 1)) , start_time() + , elapsed(0) , loading(false) , running(false) , done(false) { + title_layout.SetWidth(640 - padding.Horizontal()); + channel_layout.SetWidth(640 - padding.Horizontal()); } public: @@ -36,6 +55,16 @@ public: return done; } + void SetTitleFont(pango::Font &font) { + title_layout.SetFont(font); + text_dirty = true; + } + + void SetChannelFont(pango::Font &font) { + channel_layout.SetFont(font); + text_dirty = true; + } + void FetchClip(ws::TwitchConnection &twitch) { loading = true; twitch.FetchClips(channel.twitch_id) @@ -56,6 +85,35 @@ public: void Start(const Clock &clock); void Update(cairo::Context &ctx, const Clock &clock) { + if (text_dirty) { + ctx.UpdateLayout(title_layout); + ctx.UpdateLayout(channel_layout); + text_box.h = double(title_layout.GetLogicalRect().height) + double(channel_layout.GetLogicalRect().height) + padding.Vertical(2); + channel_pos.y = title_pos.y + double(title_layout.GetLogicalRect().height) + padding.v_inter; + text_dirty = false; + } + const Clock diff(clock.Difference(start_time)); + elapsed = std::max(0.0, std::min(1.0, double(diff.GetMS()) / (std::max(1.0, clip.GetDuration()) * 1000.0))); + } + + void Render(cairo::Context &ctx) const { + ctx.SetSourceColor(bg_color); + ctx.Rectangle(text_box); + ctx.Fill(); + + ctx.MoveTo(title_pos); + ctx.SetSourceColor(title_color); + ctx.DrawLayout(title_layout); + + ctx.MoveTo(channel_pos); + ctx.SetSourceColor(channel_color); + ctx.DrawLayout(channel_layout); + + ctx.SetSourceColor(progress_color); + ctx.SetLineWidth(2.0); + ctx.MoveTo({ text_box.x, text_box.y }); + ctx.LineTo({ text_box.x + (elapsed * text_box.w), text_box.y }); + ctx.Stroke(); } private: @@ -64,7 +122,23 @@ private: twitch::Clip clip; + pango::Layout title_layout; + pango::Layout channel_layout; + bool text_dirty; + + gfx::ColorRGB bg_color; + gfx::ColorRGB title_color; + gfx::ColorRGB channel_color; + gfx::ColorRGB progress_color; + + gfx::Spacing padding; + gfx::Rectangle screen; + gfx::Rectangle text_box; + gfx::Position title_pos; + gfx::Position channel_pos; + Clock start_time; + double elapsed; bool loading; bool running; bool done; diff --git a/src/app/Renderer.h b/src/app/Renderer.h index bdc6d4c..18e9916 100644 --- a/src/app/Renderer.h +++ b/src/app/Renderer.h @@ -51,7 +51,11 @@ public: media.Render(ctx); } - if (!state.GetShoutouts().empty()) { + if (!state.GetClips().empty() && state.GetClips().front().Running()) { + state.GetClips().front().Render(ctx); + } + + if (!state.GetShoutouts().empty() && state.GetShoutouts().front().Running()) { state.GetShoutouts().front().Render(ctx); } @@ -60,6 +64,8 @@ public: ClipPlayer &CreateClip(int channel_id, State &state) { ClipPlayer &player = state.AddClip(channel_id, ctx); + player.SetTitleFont(text_font); + player.SetChannelFont(channel_font); return player; } diff --git a/src/app/Shoutout.cpp b/src/app/Shoutout.cpp index 7c14b2d..59843cf 100644 --- a/src/app/Shoutout.cpp +++ b/src/app/Shoutout.cpp @@ -1,30 +1,16 @@ #include "Shoutout.h" -#include - #include "State.h" namespace app { -void Shoutout::LoadRandomClip(const Json::Value &json) { - const Json::Value &data = json["data"]; - if (!data.isArray()) return; - if (data.empty()) return; - Json::ArrayIndex num = data.size(); - std::uniform_int_distribution dist(0, num - 1); - Json::ArrayIndex choice = dist(state.GetRNG()); - const Json::Value &clip_json = data[choice]; - clip = twitch::Clip(clip_json); -} - void Shoutout::Start(const Clock &clock) { start_time = clock; running = true; - if (clip.HasVideo()) { - std::cout << "adding clip " << clip.GetVideoURL() << " at " << clock << std::endl; - Media &media = state.AddMedia(clip.GetVideoURL().c_str()); - media.SetSyncPoint(clock); - media.AddWindow({ 0, 0, 1, 1 }, { 320, 150, 640, 360 }); + if (channel.twitch_live) { + state.QueueIRCMessage(channel.title + " ist jetzt live mit " + channel.twitch_category + ": " + channel.twitch_title + " " + channel.stream_link); + } else { + state.QueueIRCMessage("Schaut mal bei " + channel.title + " vorbei! Da gibt's " + channel.twitch_category + " " + channel.stream_link); } } diff --git a/src/app/Shoutout.h b/src/app/Shoutout.h index 0bd3694..5355f11 100644 --- a/src/app/Shoutout.h +++ b/src/app/Shoutout.h @@ -7,9 +7,6 @@ #include "../gfx/ColorRGB.h" #include "../gfx/Position.h" #include "../gfx/Spacing.h" -#include "../twitch/Clip.h" -#include "../ws/TwitchConnection.h" -#include "json/value.h" #include namespace app { @@ -31,15 +28,14 @@ public: , live_color{ 0.8, 0.1, 0.1 } , title_color{ 1, 1, 1 } , channel_color{ 0.392, 0.255, 0.647 } + , category_color{ 0.6, 0.6, 0.6 } , anchor{ 1280, 720 - 75 } , size{ 1280, 720 } , padding(10) - , category_color{ 0.6, 0.6, 0.6 } , start_time() , running(false) - , done(false) - , fetching_clip(false) { - live_layout.SetText("Jetzt Live"); + , done(false) { + live_layout.SetText(channel.twitch_live ? "Jetzt Live!" : "Auch gut:"); title_layout.SetText(channel.twitch_title); channel_layout.SetText(channel.title); category_layout.SetText(channel.twitch_category); @@ -62,7 +58,7 @@ public: public: bool Loading() const { - return fetching_clip; + return false; } bool Running() const { @@ -89,22 +85,6 @@ public: category_layout.SetFont(font); } - void FetchClip(ws::TwitchConnection &twitch) { - fetching_clip = true; - twitch.FetchClips(channel.twitch_id) - .Then([this](const Json::Value *rsp) -> void { - fetching_clip = false; - LoadRandomClip(*rsp); - }) - .Catch([this](ws::HttpsConnection *rsp) -> void { - fetching_clip = false; - std::cout << "failed to fetch clips" << std::endl; - std::cout << rsp->GetBody() << std::endl; - }); - } - - void LoadRandomClip(const Json::Value &json); - void Start(const Clock &clock); void Update(cairo::Context &ctx, const Clock &clock) { @@ -205,12 +185,9 @@ private: gfx::Size size; gfx::Spacing padding; - twitch::Clip clip; - Clock start_time; bool running; bool done; - bool fetching_clip; }; diff --git a/src/app/State.h b/src/app/State.h index fb24c2e..5af3e37 100644 --- a/src/app/State.h +++ b/src/app/State.h @@ -1,12 +1,12 @@ #ifndef TEST_APP_STATE_H_ #define TEST_APP_STATE_H_ +#include #include #include #include #include #include -#include #include "ChannelInfo.h" #include "ClipPlayer.h" @@ -68,6 +68,9 @@ public: if (strcasecmp(c.second.title.c_str(), str.c_str()) == 0) { return &c.second; } + if (strcasecmp(c.second.short_name.c_str(), str.c_str()) == 0) { + return &c.second; + } if (c.second.twitch_chat.length() > 1 && strcasecmp(c.second.twitch_chat.c_str() + 1, str.c_str()) == 0) { return &c.second; } @@ -75,6 +78,10 @@ public: return nullptr; } + const std::list &GetClips() const { + return clips; + } + ChannelInfo *GetRandomChannel() { if (channels.empty()) { return nullptr; @@ -123,6 +130,29 @@ public: return shoutouts.back(); } + void QueueIRCMessage(const std::string &msg) { + irc_queue.push_back(msg); + } + + bool IRCMessageReady() const { + if (irc_queue.empty()) { + return false; + } + const auto now = std::chrono::high_resolution_clock::now(); + auto diff = std::chrono::duration_cast(now - irc_last_msg).count(); + return diff > 600; + } + + std::string PopIRCMessage() { + if (irc_queue.empty()) { + return ""; + } + std::string msg = irc_queue.front(); + irc_queue.pop_front(); + irc_last_msg = std::chrono::high_resolution_clock::now(); + return msg; + } + int GetWidth() const { return width; } @@ -166,6 +196,7 @@ public: clips.front().Update(ctx, clock); } else if (!clips.front().Loading()) { clips.front().Start(clock); + clips.front().Update(ctx, clock); } } if (!shoutouts.empty()) { @@ -175,6 +206,7 @@ public: shoutouts.front().Update(ctx, clock); } else if (!shoutouts.front().Loading()) { shoutouts.front().Start(clock); + shoutouts.front().Update(ctx, clock); } } } @@ -207,6 +239,9 @@ private: std::list msgs; std::list shoutouts; + std::list irc_queue; + std::chrono::time_point irc_last_msg; + std::random_device rnd_dev; std::mt19937 rnd_gen; diff --git a/src/twitch/Clip.h b/src/twitch/Clip.h index e9d7172..3e47db4 100644 --- a/src/twitch/Clip.h +++ b/src/twitch/Clip.h @@ -15,7 +15,9 @@ public: : broadcaster_name(json["broadcaster_name"].asString()) , creator_name(json["creator_name"].asString()) , thumbnail_url(json["thumbnail_url"].asString()) - , title(json["title"].asString()) { + , title(json["title"].asString()) + , duration(json["duration"].asDouble()) + , url(json["url"].asString()) { std::cout << "clip: " << json << std::endl; size_t thumb_pos = thumbnail_url.find("-preview-"); if (thumb_pos != std::string::npos) { @@ -29,6 +31,22 @@ public: return !video_url.empty(); } + const std::string &GetBroadcasterName() const { + return broadcaster_name; + } + + const std::string &GetClipURL() const { + return url; + } + + double GetDuration() const { + return duration; + } + + const std::string &GetTitle() const { + return title; + } + const std::string &GetVideoURL() const { return video_url; } @@ -38,6 +56,8 @@ private: std::string creator_name; std::string thumbnail_url; std::string title; + double duration; + std::string url; std::string video_url; };