]> git.localhorst.tv Git - ffmpeg-test.git/commitdiff
enhanced clip player
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Thu, 21 Nov 2024 18:34:46 +0000 (19:34 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Thu, 21 Nov 2024 18:34:46 +0000 (19:34 +0100)
src/app/Application.h
src/app/ChannelInfo.h
src/app/ClipPlayer.cpp
src/app/ClipPlayer.h
src/app/Renderer.h
src/app/Shoutout.cpp
src/app/Shoutout.h
src/app/State.h
src/twitch/Clip.h

index 1d7d6b6e448ea1268343684b9a2370ccb0f8d50d..200b36c010d09b712959320446ccf67b955b62c5 100644 (file)
@@ -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()) {
index e41722a53b781dd394869251c166245e64acd079..c782b192d340499b96edc08cc394579ee1f7a63f 100644 (file)
@@ -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;
index 988207a0f199585cc86802e2a493206bb257ec15..ea840b8a4c763148dd3b48d6517e62d83e939532 100644 (file)
@@ -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<std::mt19937::result_type> 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;
        }
index e201651bb1eff98fce1e2d593f1f4f69def4c611..6eb9d4bdd02a84999ce4c1b452ef3e0aa1a79aee 100644 (file)
@@ -1,9 +1,13 @@
 #ifndef TEST_APP_CLIPPLAYER_H_
 #define TEST_APP_CLIPPLAYER_H_
 
+#include <algorithm>
+
 #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;
index bdc6d4c801d272b2dfa83ecb1fecaef19d969ee9..18e9916f8b1166f11152620cb61514e5114f8533 100644 (file)
@@ -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;
        }
 
index 7c14b2d690a05905ea86b68ecb2f55dcc7aa6114..59843cf28d60fdba7e589812b6dd0fc918c397aa 100644 (file)
@@ -1,30 +1,16 @@
 #include "Shoutout.h"
 
-#include <random>
-
 #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<std::mt19937::result_type> 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);
        }
 }
 
index 0bd36947b15a392b44ab8d018f8f000fe8d550a9..5355f118af0a0a2feaf1c52de16aae1979df12cc 100644 (file)
@@ -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 <cstdint>
 
 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;
 
 };
 
index fb24c2e05f3b698a657630b06e62408f43f584a5..5af3e372267056324e254d7c48f319b879f92b20 100644 (file)
@@ -1,12 +1,12 @@
 #ifndef TEST_APP_STATE_H_
 #define TEST_APP_STATE_H_
 
+#include <chrono>
 #include <iterator>
 #include <list>
 #include <map>
 #include <ostream>
 #include <random>
-#include <unicode/unistr.h>
 
 #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<ClipPlayer> &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<std::chrono::milliseconds>(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<Message> msgs;
        std::list<Shoutout> shoutouts;
 
+       std::list<std::string> irc_queue;
+       std::chrono::time_point<std::chrono::system_clock> irc_last_msg;
+
        std::random_device rnd_dev;
        std::mt19937 rnd_gen;
 
index e9d7172e6571329ac1e89b9c6ec5747b22a73cf0..3e47db4cb5715773e6091e1f1c5e2aa765bc2636 100644 (file)
@@ -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;
 
 };