Skip to content

Commit

Permalink
Merge pull request #16623 from schlawg/relay-pinned-streamer-image-link
Browse files Browse the repository at this point in the history
aaarmstark's pinned streamer image link
  • Loading branch information
ornicar authored Dec 22, 2024
2 parents 17f3d89 + 5e22fcf commit 3227175
Show file tree
Hide file tree
Showing 17 changed files with 260 additions and 111 deletions.
17 changes: 9 additions & 8 deletions app/controllers/RelayRound.scala
Original file line number Diff line number Diff line change
Expand Up @@ -268,20 +268,21 @@ final class RelayRound(
isSubscribed <- ctx.me.soFu: me =>
env.relay.api.isSubscribed(rt.tour.id, me.userId)
videoUrls <- embed match
case VideoEmbed.Auto =>
fuccess:
rt.tour.pinnedStream
.ifFalse(rt.round.isFinished)
.flatMap(_.upstream)
.map(_.urls(netDomain).toPair)
case VideoEmbed.No => fuccess(none)
case VideoEmbed.Stream(userId) =>
env.streamer.api
.find(userId)
.flatMapz(s => env.streamer.liveStreamApi.of(s).dmap(some))
.map:
_.flatMap(_.stream).map(_.urls(netDomain).toPair)
crossSiteIsolation = videoUrls.isEmpty
case VideoEmbed.PinnedStream =>
fuccess:
rt.tour.pinnedStream
.ifFalse(rt.round.isFinished)
.flatMap(_.upstream)
.map(_.urls(netDomain).toPair)
case _ => fuccess(none)
crossSiteIsolation = videoUrls.isEmpty || (rt.tour.pinnedStream.isDefined && crossOriginPolicy
.supportsCredentiallessIFrames(ctx.req))
data = env.relay.jsonView.makeData(
rt.tour.withRounds(rounds.map(_.round)),
rt.round.id,
Expand Down
8 changes: 6 additions & 2 deletions modules/relay/src/main/JsonView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,12 @@ final class JsonView(baseUrl: BaseUrl, markup: RelayMarkup, picfitUrl: PicfitUrl
.add("lcc", trs.rounds.find(_.id == currentRoundId).map(_.sync.upstream.exists(_.hasLcc)))
.add("isSubscribed" -> isSubscribed)
.add("videoUrls" -> videoUrls)
.add("pinnedStream" -> pinned)
.add("note" -> trs.tour.note.ifTrue(canContribute)),
.add("note" -> trs.tour.note.ifTrue(canContribute))
.add("pinned" -> pinned.map: p =>
Json
.obj("name" -> p.name)
.add("redirect" -> p.upstream.map(_.urls(lila.core.config.NetDomain("")).redirect))
.add("text" -> p.text)),
study = studyData.study,
analysis = studyData.analysis,
group = group.map(_.group.name)
Expand Down
19 changes: 8 additions & 11 deletions modules/relay/src/main/RelayPinnedStream.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,19 @@ import io.mola.galimatias.URL
import scala.jdk.CollectionConverters.*
import lila.core.config.NetDomain

case class RelayPinnedStream(name: String, url: URL):
case class RelayPinnedStream(name: String, url: URL, text: Option[String]):

import RelayPinnedStream.*

def upstream: Option[RelayPinnedStream.Upstream] =
parseYoutube.orElse(parseTwitch)

// https://www.youtube.com/live/Lg0askmGqvo
// https://www.youtube.com/live/Lg0askmGqvo?si=KKOexnmA2xPcyStZ
def parseYoutube: Option[YouTube] =
url.host.toString
.endsWith("youtube.com")
.so:
url.pathSegments.asScala.toList match
case List("live", id) => YouTube(id).some
case _ => none
if List("www.youtube.com", "youtube.com", "youtu.be").contains(url.host.toString) then
url.pathSegments.asScala.toList match
case List("live", id) => Some(YouTube(id))
case _ => Option(url.queryParameter("v")).map(YouTube.apply)
else None

// https://www.twitch.tv/tcec_chess_tv
def parseTwitch: Option[Twitch] =
Expand All @@ -37,11 +34,11 @@ object RelayPinnedStream:
def urls(parent: NetDomain): Urls
case class YouTube(id: String) extends Upstream:
def urls(parent: NetDomain) = Urls(
s"https://www.youtube.com/embed/${id}?disablekb=1&modestbranding=1",
s"https://www.youtube.com/embed/${id}?disablekb=1&modestbranding=1&autoplay=1",
s"https://www.youtube.com/watch?v=${id}"
)
case class Twitch(id: String) extends Upstream:
def urls(parent: NetDomain) = Urls(
s"https://player.twitch.tv/?channel=${id}&parent=${parent}",
s"https://player.twitch.tv/?channel=${id}&parent=${parent}&autoplay=true",
s"https://www.twitch.tv/${id}"
)
4 changes: 3 additions & 1 deletion modules/relay/src/main/RelayTourForm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ final class RelayTourForm(langList: lila.core.i18n.LangList):

private val pinnedStreamMapping = mapping(
"name" -> cleanNonEmptyText(maxLength = 100),
"url" -> url.field.verifying("Invalid stream URL", url => RelayPinnedStream("", url).upstream.isDefined)
"url" -> url.field
.verifying("Invalid stream URL", url => RelayPinnedStream("", url, None).upstream.isDefined),
"text" -> optional(cleanText(maxLength = 100))
)(RelayPinnedStream.apply)(unapply)

private given Formatter[RelayTour.Tier] =
Expand Down
10 changes: 5 additions & 5 deletions modules/relay/src/main/RelayVideoEmbed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import play.api.mvc.Result
enum RelayVideoEmbed:
case No
case Auto
case PinnedStream
case Stream(userId: UserId)
override def toString = this match
case No => "no"
case Auto => ""
case Stream(u) => u.toString
case No => "no"
case _ => ""

final class RelayVideoEmbedStore(baker: LilaCookie):

Expand All @@ -21,11 +21,11 @@ final class RelayVideoEmbedStore(baker: LilaCookie):
def read(using req: RequestHeader): RelayVideoEmbed =
def fromCookie = req.cookies.get(cookieName).map(_.value).filter(_.nonEmpty) match
case Some("no") => No
case Some("ps") => PinnedStream
case _ => Auto
req.queryString.get("embed") match
case Some(Nil) => fromCookie
case Some(Seq("")) => Auto
case Some(Seq("no")) => No
case Some(Seq("ps")) => PinnedStream
case Some(Seq(name)) => UserStr.read(name).fold(Auto)(u => Stream(u.id))
case _ => fromCookie

Expand Down
11 changes: 11 additions & 0 deletions modules/relay/src/main/ui/RelayFormUi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,17 @@ Team Dogs ; Scooby Doo"""),
"Stream name",
half = true
)(form3.input(_))
),
form3.split(
form3.group(
form("pinnedStream.text"),
"Stream link label",
help = frag(
"Optional. Show a label on the image link to your live stream.",
br,
"Example: 'Watch us live on YouTube!'"
).some
)(form3.input(_))
)
)
)
Expand Down
2 changes: 1 addition & 1 deletion ui/analyse/css/study/relay/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ body {
main.is-relay {
.relay-tour {
grid-area: relay-tour;
overflow: visible;
&__side {
grid-area: side;
}
Expand All @@ -22,7 +23,6 @@ main.is-relay {
}

@include mq-at-least-col3 {
#video-player-placeholder,
button.streamer-show {
display: block;
}
Expand Down
5 changes: 4 additions & 1 deletion ui/analyse/css/study/relay/_tour.scss
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,13 @@ $hover-bg: $m-primary_bg--mix-30;
flex: 0 0 50%;
}
line-height: 0;
img {
> img {
width: 100%;
@include broken-img(2 / 1);
}
.video-player-close {
display: none;
}
text-align: center;
}
&__image-upload {
Expand Down
91 changes: 87 additions & 4 deletions ui/analyse/css/study/relay/_video-player.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,99 @@

#video-player-placeholder {
aspect-ratio: 16/9;
position: relative;
width: 100%;
}

img.video-player-close {
.video-player-close {
z-index: $z-video-player-controls-101;
position: absolute;
height: 20px;
width: 20px;
pointer-events: auto;
top: 6px;
right: 6px;
height: 24px;
width: 24px;
padding: 2px;
border-radius: 50%;
background-color: black;
cursor: pointer;
&:hover {
filter: brightness(10);
filter: brightness(3);
}
}

#video-player-placeholder.link {
cursor: pointer;
overflow: hidden;
outline-offset: -3px;
outline: 3px solid $m-bad--alpha-50;

.image {
position: absolute;
background: center / cover;
overflow: hidden;
inset: 0;
filter: blur(4px) brightness(0.7);
}

.play-button {
position: absolute;
pointer-events: none;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
width: 18%;
opacity: 0.6;
fill: white;
filter: drop-shadow(0 0 12px #0000004f);

circle {
filter: drop-shadow(0 0 8px #840000);
paint-order: stroke fill;
stroke: #fff9;
stroke-width: 3px;
fill: $c-bad;
}
}

&:has(.text-box) .play-button {
top: 56%;
}

.text-box {
@extend %flex-column;
position: absolute;
pointer-events: none;
justify-content: center;
align-items: center;
top: 10%;
left: 10%;
right: 10%;
}

.text-box div {
margin: auto;
pointer-events: none;
border-radius: 5px;
border: 1px solid #8888;
padding: 5px 8px;
text-align: center;
line-height: normal;
color: #ddde;
background-color: #333d;
font-family: 'Noto Sans';
font-size: 1.2em;
}

&:hover:not(:has(.video-player-close:hover)) {
box-shadow: 0 0 12px $c-bad;

.play-button {
opacity: 1;
}

.image {
filter: blur(4px) brightness(0.6);
}
}
}
5 changes: 2 additions & 3 deletions ui/analyse/src/analyse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { wsConnect } from 'common/socket';

export { patch };

export const start = makeStart(patch);

export const boot = makeBoot(start);
const start = makeStart(patch);
const boot = makeBoot(start);

export function initModule({ mode, cfg }: { mode: 'userAnalysis' | 'replay'; cfg: any }) {
if (mode === 'replay') boot(cfg);
Expand Down
2 changes: 2 additions & 0 deletions ui/analyse/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ export interface AnalyseOpts {
inlinePgn?: string;
externalEngineEndpoint: string;
embed?: boolean;
socketUrl?: string;
socketVersion?: number;
}

export interface JustCaptured extends Piece {
Expand Down
12 changes: 6 additions & 6 deletions ui/analyse/src/plugins/analyse.study.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { patch } from '../view/util';
import makeBoot from '../boot';
import makeStart from '../start';
import type { AnalyseOpts } from '../interfaces';
import type { AnalyseSocketSend } from '../socket';
import * as studyDeps from '../study/studyDeps';
import { wsConnect } from 'common/socket';

export { patch };

export const start = makeStart(patch, studyDeps);
export const boot = makeBoot(start);
const start = makeStart(patch, studyDeps);

export function initModule(cfg: any) {
cfg.socketSend = wsConnect(cfg.socketUrl || '/analysis/socket/v5', cfg.socketVersion, {
export function initModule(cfg: AnalyseOpts) {
cfg.socketSend = wsConnect(cfg.socketUrl || '/analysis/socket/v5', cfg.socketVersion ?? false, {
receive: (t: string, d: any) => analyse.socketReceive(t, d),
...(cfg.embed ? { params: { flag: 'embed' } } : {}),
}).send;
}).send as AnalyseSocketSend;
const analyse = start(cfg);
}
2 changes: 1 addition & 1 deletion ui/analyse/src/study/relay/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface RelayData {
group?: RelayGroup;
isSubscribed?: boolean; // undefined if anon
videoUrls?: [string, string];
pinnedStream?: { name: string; youtube?: string; twitch?: string };
pinned?: { name: string; redirect: string; text?: string };
note?: string;
lcc?: boolean;
}
Expand Down
36 changes: 25 additions & 11 deletions ui/analyse/src/study/relay/relayCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { RelayData, LogEvent, RelaySync, RelayRound, RoundId } from './inte
import type { BothClocks, ChapterId, ChapterSelect, Federations, ServerClockMsg } from '../interfaces';
import type { StudyMemberCtrl } from '../studyMembers';
import type { AnalyseSocketSend } from '../../socket';
import { type Prop, type Toggle, defined, myUserId, notNull, prop, toggle } from 'common';
import { type Prop, type Toggle, myUserId, notNull, prop, toggle } from 'common';
import RelayTeams from './relayTeams';
import RelayPlayers from './relayPlayers';
import type { StudyChapters } from '../studyChapters';
Expand Down Expand Up @@ -59,19 +59,27 @@ export default class RelayCtrl {
redraw,
);
this.stats = new RelayStats(this.currentRound(), redraw);
this.videoPlayer = this.data.videoUrls?.[0] ? new VideoPlayer(this.data.videoUrls[0], redraw) : undefined;
setInterval(() => this.redraw(true), 1000);

const pinned = data.pinnedStream;
if (pinned && this.pinStreamer()) this.streams.push(['', pinned.name]);
if (data.videoUrls?.[0] || this.isPinnedStreamOngoing())
this.videoPlayer = new VideoPlayer(
{
embed: this.data.videoUrls?.[0] || false,
redirect: this.data.videoUrls?.[1] || this.data.pinned?.redirect,
image: this.data.tour.image,
text: this.data.pinned?.text,
},
redraw,
);
const pinnedName = this.isPinnedStreamOngoing() && data.pinned?.name;
if (pinnedName) this.streams.push(['ps', pinnedName]);

pubsub.on('socket.in.crowd', d => {
const s = (d.streams as [string, string][]) ?? [];
if (pinned && this.pinStreamer()) s.unshift(['', pinned.name]);
if (pinnedName) s.unshift(['ps', pinnedName]);
if (this.streams.length === s.length && this.streams.every(([id], i) => id === s[i][0])) return;
this.streams = s;
this.redraw();
});
setInterval(() => this.redraw(true), 1000);
}

openTab = (t: RelayTab) => {
Expand Down Expand Up @@ -131,10 +139,16 @@ export default class RelayCtrl {

isStreamer = () => this.streams.some(([id]) => id === myUserId());

pinStreamer = () =>
defined(this.data.pinnedStream) &&
!this.currentRound().finished &&
Date.now() > this.currentRound().startsAt! - 1000 * 3600;
isPinnedStreamOngoing = () => {
if (!this.data.pinned) return false;
if (this.currentRound().finished) return false;
if (Date.now() < this.currentRound().startsAt! - 1000 * 3600) return false;
return true;
};

noEmbed() {
return document.cookie.includes('relayVideo=no');
}

private socketHandlers = {
relayData: (d: RelayData) => {
Expand Down
Loading

0 comments on commit 3227175

Please sign in to comment.