improve tv stream

puzzle
Thibault Duplessis 2020-12-20 18:26:48 +01:00
parent fa46838ce6
commit 4fcec2105b
9 changed files with 84 additions and 47 deletions

View File

@ -72,12 +72,13 @@ final class Tv(
}
def feed =
Action.async {
Action.async { req =>
import makeTimeout.short
import akka.pattern.ask
import lila.round.TvBroadcast
import play.api.libs.EventSource
env.round.tvBroadcast ? TvBroadcast.Connect mapTo
val fromLichess = getBool("bc", req)
env.round.tvBroadcast ? TvBroadcast.Connect(fromLichess) mapTo
manifest[TvBroadcast.SourceType] map { source =>
Ok.chunked(source via EventSource.flow).as(ContentTypes.EVENT_STREAM) pipe noProxyBuffer
}

View File

@ -8,14 +8,14 @@ import lila.app.ui.ScalatagsTemplate._
object embed {
private val dataStreamUrl = attr("data-stream-url")
private val dataStreamUrl = attr("data-stream-url") := "/tv/feed?bc=1"
def apply(pov: lila.game.Pov)(implicit config: EmbedConfig) =
views.html.base.embed(
title = "lichess.org chess TV",
cssModule = "tv.embed"
)(
dataStreamUrl := routes.Tv.feed(),
dataStreamUrl,
div(id := "featured-game", cls := "embedded", title := "lichess.org TV")(
views.html.game.mini.noCtx(pov, tv = true, blank = true)
),

View File

@ -187,11 +187,6 @@ package timeline {
}
}
package game {
case class ChangeFeatured(id: String, msg: JsObject)
case object Count
}
package tv {
case class TvSelect(gameId: String, speed: chess.Speed, data: JsObject)
}

View File

@ -6,7 +6,6 @@ import scala.concurrent.duration._
import scala.concurrent.Promise
import lila.game.Pov
import lila.hub.actorApi.game.ChangeFeatured
import lila.hub.actorApi.timeline._
import lila.hub.Trouper
import lila.i18n.defaultLang
@ -15,6 +14,7 @@ import lila.rating.RatingRange
import lila.socket.RemoteSocket.{ Protocol => P, _ }
import lila.socket.Socket.{ makeMessage, Sri, Sris }
import lila.user.User
import lila.round.ChangeFeatured
case class LobbyCounters(members: Int, rounds: Int)

View File

@ -50,6 +50,7 @@ final class Env(
evalCache: lila.evalCache.EvalCacheApi,
remoteSocketApi: lila.socket.RemoteSocket,
isBotSync: lila.common.LightUser.IsBotSync,
lightUserSync: lila.common.LightUser.GetterSync,
slackApi: lila.slack.SlackApi,
ratingFactors: () => lila.rating.RatingFactors,
shutdown: akka.actor.CoordinatedShutdown
@ -179,7 +180,7 @@ final class Env(
val playing = wire[PlayingUsers]
val tvBroadcast = system.actorOf(Props(classOf[TvBroadcast]))
val tvBroadcast = system.actorOf(Props(wire[TvBroadcast]))
def resign(pov: Pov): Unit =
if (pov.game.abortable) tellRound(pov.gameId, Abort(pov.playerId))

View File

@ -1,8 +1,10 @@
package lila.round
import scala.math
import actorApi.SocketStatus
import chess.format.{ FEN, Forsyth }
import chess.{ Clock, Color }
import play.api.libs.json._
import scala.math
import lila.common.ApiVersion
import lila.game.JsonView._
@ -10,11 +12,6 @@ import lila.game.{ Pov, Game, Player => GamePlayer }
import lila.pref.Pref
import lila.user.{ User, UserRepo }
import chess.format.{ FEN, Forsyth }
import chess.{ Clock, Color }
import actorApi.SocketStatus
final class JsonView(
userRepo: UserRepo,
userJsonView: lila.user.JsonView,
@ -38,9 +35,7 @@ final class JsonView(
private def commonPlayerJson(g: Game, p: GamePlayer, user: Option[User], withFlags: WithFlags): JsObject =
Json
.obj(
"color" -> p.color.name
)
.obj("color" -> p.color.name)
.add("user" -> user.map { userJsonView.minimal(_, g.perfType) })
.add("rating" -> p.rating)
.add("ratingDiff" -> p.ratingDiff)

View File

@ -2,20 +2,27 @@ package lila.round
import akka.actor._
import akka.stream.scaladsl._
import lila.game.actorApi.MoveGameEvent
import lila.hub.actorApi.game.ChangeFeatured
import lila.socket.Socket.makeMessage
import chess.format.FEN
import chess.format.Forsyth
import play.api.libs.json._
import lila.common.Bus
import lila.common.LightUser
import lila.game.actorApi.MoveGameEvent
import lila.game.Game
import lila.socket.Socket
import lila.socket.Socket.makeMessage
final private class TvBroadcast extends Actor {
final private class TvBroadcast(
userJsonView: lila.user.JsonView,
lightUserSync: LightUser.GetterSync
) extends Actor {
import TvBroadcast._
private var queues = Set.empty[Queue]
private var clients = Set.empty[Client]
private var featuredId = none[String]
private var featured = none[Featured]
Bus.subscribe(self, "changeFeaturedGame")
@ -28,26 +35,51 @@ final private class TvBroadcast extends Actor {
def receive = {
case TvBroadcast.Connect =>
case TvBroadcast.Connect(compat) =>
sender() ! Source
.queue[JsValue](8, akka.stream.OverflowStrategy.dropHead)
.mapMaterializedValue { queue =>
self ! Add(queue)
val client = Client(queue, compat)
self ! Add(client)
queue.watchCompletion().foreach { _ =>
self ! Remove(queue)
self ! Remove(client)
}
featured ifFalse compat foreach { f =>
client.queue.offer(Socket.makeMessage("featured", f.dataWithFen))
}
}
case Add(queue) => queues = queues + queue
case Remove(queue) => queues = queues - queue
case Add(client) => clients = clients + client
case Remove(client) => clients = clients - client
case ChangeFeatured(id, msg) =>
case ChangeFeatured(pov, msg) =>
unsubscribeFromFeaturedId()
Bus.subscribe(self, MoveGameEvent makeChan id)
featuredId = id.some
queues.foreach(_ offer msg)
Bus.subscribe(self, MoveGameEvent makeChan pov.gameId)
val feat = Featured(
pov.gameId,
Json.obj(
"id" -> pov.gameId,
"orientation" -> pov.color.name,
"players" -> pov.game.players.map { p =>
val user = p.userId.flatMap(lightUserSync)
Json
.obj("color" -> p.color.name)
.add("user" -> user.map(LightUser.lightUserWrites.writes))
.add("ai" -> p.aiLevel)
.add("rating" -> p.rating)
}
),
fen = Forsyth exportBoard pov.game.chess.board
)
clients.foreach { client =>
client.queue offer {
if (client.fromLichess) msg
else feat.socketMsg
}
}
featured = feat.some
case MoveGameEvent(game, fen, move) if queues.nonEmpty =>
case MoveGameEvent(game, fen, move) =>
val msg = makeMessage(
"fen",
Json
@ -58,12 +90,15 @@ final private class TvBroadcast extends Actor {
.add("wc" -> game.clock.map(_.remainingTime(chess.White).roundSeconds))
.add("bc" -> game.clock.map(_.remainingTime(chess.Black).roundSeconds))
)
queues.foreach(_ offer msg)
clients.foreach(_.queue offer msg)
featured foreach { f =>
featured = f.copy(fen = fen).some
}
}
def unsubscribeFromFeaturedId() =
featuredId foreach { previous =>
Bus.unsubscribe(self, MoveGameEvent makeChan previous)
featured foreach { previous =>
Bus.unsubscribe(self, MoveGameEvent makeChan previous.id)
}
}
@ -72,8 +107,14 @@ object TvBroadcast {
type SourceType = Source[JsValue, _]
type Queue = SourceQueueWithComplete[JsValue]
case object Connect
case class Featured(id: Game.ID, data: JsObject, fen: String) {
def dataWithFen = data ++ Json.obj("fen" -> fen)
def socketMsg = Socket.makeMessage("featured", dataWithFen)
}
case class Add(q: Queue)
case class Remove(q: Queue)
case class Connect(fromLichess: Boolean)
case class Client(queue: Queue, fromLichess: Boolean)
case class Add(c: Client)
case class Remove(c: Client)
}

View File

@ -4,6 +4,7 @@ import scala.concurrent.duration.FiniteDuration
import lila.game.{ Game, Pov }
import lila.user.User
import play.api.libs.json.JsObject
private case class MoretimeDuration(value: FiniteDuration) extends AnyVal
private case class AnimationDuration(value: FiniteDuration) extends AnyVal
@ -27,3 +28,5 @@ final private class ScheduleExpiration(f: Game => Unit) extends (Game => Unit) {
final class IsOfferingRematch(f: Pov => Boolean) extends (Pov => Boolean) {
def apply(p: Pov) = f(p)
}
case class ChangeFeatured(pov: Pov, mgs: JsObject)

View File

@ -6,7 +6,7 @@ import scala.concurrent.duration._
import scala.concurrent.Promise
import lila.common.{ Bus, LightUser }
import lila.game.Game
import lila.game.{ Game, Pov }
import lila.hub.Trouper
final private[tv] class TvTrouper(
@ -79,13 +79,14 @@ final private[tv] class TvTrouper(
if (channel == Tv.Channel.Best) {
implicit def timeout = makeTimeout(100 millis)
actorAsk(renderer.actor, actorApi.RenderFeaturedJs(game)) foreach { case html: String =>
val event = lila.hub.actorApi.game.ChangeFeatured(
game.id,
val pov = Pov naturalOrientation game
val event = lila.round.ChangeFeatured(
pov,
makeMessage(
"featured",
Json.obj(
"html" -> html,
"color" -> game.naturalOrientation.name,
"color" -> pov.color.name,
"id" -> game.id
)
)