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() {
stream.PushAudioFrame();
}
+ if (state.IRCMessageReady()) {
+ SendIRCText(state.PopIRCMessage());
+ }
+
state.Clean();
if (enable_realtime && difference > 1000) {
if (config.shoutouts) {
twitch_conn.Shoutout(config.own_channel_id, channel.twitch_id);
}
- shout.FetchClip(twitch_conn);
+ EnqueueClip(id);
}
}
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;
if (channel) {
EnqueueClip(channel->id);
} else {
- SendIRCText("Channel nicht gefunden NotLikeThis");
+ state.QueueIRCMessage("Channel nicht gefunden NotLikeThis");
}
}
if (state.HasGame()) {
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())
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();
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;
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());
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;
}
#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"
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:
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)
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:
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;
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);
}
ClipPlayer &CreateClip(int channel_id, State &state) {
ClipPlayer &player = state.AddClip(channel_id, ctx);
+ player.SetTitleFont(text_font);
+ player.SetChannelFont(channel_font);
return player;
}
#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);
}
}
#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 {
, 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);
public:
bool Loading() const {
- return fetching_clip;
+ return false;
}
bool Running() const {
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) {
gfx::Size size;
gfx::Spacing padding;
- twitch::Clip clip;
-
Clock start_time;
bool running;
bool done;
- bool fetching_clip;
};
#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"
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;
}
return nullptr;
}
+ const std::list<ClipPlayer> &GetClips() const {
+ return clips;
+ }
+
ChannelInfo *GetRandomChannel() {
if (channels.empty()) {
return nullptr;
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;
}
clips.front().Update(ctx, clock);
} else if (!clips.front().Loading()) {
clips.front().Start(clock);
+ clips.front().Update(ctx, clock);
}
}
if (!shoutouts.empty()) {
shoutouts.front().Update(ctx, clock);
} else if (!shoutouts.front().Loading()) {
shoutouts.front().Start(clock);
+ shoutouts.front().Update(ctx, clock);
}
}
}
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;
: 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) {
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;
}
std::string creator_name;
std::string thumbnail_url;
std::string title;
+ double duration;
+ std::string url;
std::string video_url;
};