diff --git a/pkg/api/worker.go b/pkg/api/worker.go index d933c8ea..a071fa17 100644 --- a/pkg/api/worker.go +++ b/pkg/api/worker.go @@ -55,10 +55,11 @@ type ( InitWebrtcStreamResponse string AppVideoInfo struct { - W int `json:"w"` - H int `json:"h"` - S int `json:"s"` - A float32 `json:"a"` + W int `json:"w"` + H int `json:"h"` + S int `json:"s"` + A float32 `json:"a"` + Flip bool `json:"flip,omitempty"` } LibGameListInfo struct { diff --git a/pkg/encoder/encoder.go b/pkg/encoder/encoder.go index 5aa3886a..2252f5e0 100644 --- a/pkg/encoder/encoder.go +++ b/pkg/encoder/encoder.go @@ -18,7 +18,6 @@ type ( Encode([]byte) []byte IntraRefresh() Info() string - SetFlip(bool) Shutdown() error } ) @@ -119,14 +118,6 @@ func (v *Video) SetRot(a uint) { } } -// SetFlip tells the encoder to flip the frames vertically. -func (v *Video) SetFlip(b bool) { - if v == nil { - return - } - v.codec.SetFlip(b) -} - func (v *Video) Stop() { if v == nil { return diff --git a/pkg/encoder/h264/x264.go b/pkg/encoder/h264/x264.go index 08c310cd..9429b926 100644 --- a/pkg/encoder/h264/x264.go +++ b/pkg/encoder/h264/x264.go @@ -193,14 +193,6 @@ func (e *H264) IntraRefresh() { func (e *H264) Info() string { return fmt.Sprintf("x264: v%v", Version()) } -func (e *H264) SetFlip(b bool) { - if b { - (*e.h).pic.img.i_csp |= C.X264_CSP_VFLIP - } else { - (*e.h).pic.img.i_csp &= ^C.X264_CSP_VFLIP - } -} - func (e *H264) Shutdown() error { if e.h != nil { C.h264_destroy(e.h) diff --git a/pkg/encoder/vpx/libvpx.go b/pkg/encoder/vpx/libvpx.go index ed312e46..bc3a9f70 100644 --- a/pkg/encoder/vpx/libvpx.go +++ b/pkg/encoder/vpx/libvpx.go @@ -119,12 +119,9 @@ type Vpx struct { image C.vpx_image_t codecCtx C.vpx_codec_ctx_t kfi C.int - flipped bool v int } -func (vpx *Vpx) SetFlip(b bool) { vpx.flipped = b } - func autoThreads(configured, cpus int) int { if configured != 0 { return configured @@ -240,9 +237,6 @@ func NewEncoder(w, h int, th int, kfi int, version int, opts *Options) (*Vpx, er // see: https://chromium.googlesource.com/webm/libvpx/+/master/examples/simple_encoder.c func (vpx *Vpx) Encode(yuv []byte) []byte { C.vpx_img_read(&vpx.image, unsafe.Pointer(&yuv[0])) - if vpx.flipped { - C.vpx_img_flip(&vpx.image) - } var flags C.int if vpx.frameCount < 3 || (vpx.kfi > 0 && vpx.frameCount%vpx.kfi == 0) { diff --git a/pkg/worker/caged/app/app.go b/pkg/worker/caged/app/app.go index 74d89432..db398a61 100644 --- a/pkg/worker/caged/app/app.go +++ b/pkg/worker/caged/app/app.go @@ -4,6 +4,7 @@ type App interface { AudioSampleRate() int AspectRatio() float32 AspectEnabled() bool + Flipped() bool Init() error ViewportSize() (int, int) Scale() float64 diff --git a/pkg/worker/coordinatorhandlers.go b/pkg/worker/coordinatorhandlers.go index 72ca1044..ed1874d2 100644 --- a/pkg/worker/coordinatorhandlers.go +++ b/pkg/worker/coordinatorhandlers.go @@ -154,10 +154,11 @@ func (c *coordinator) HandleGameStart(rq api.StartGameRequest, w *Worker) api.Ou data, err := api.Wrap(api.Out{ T: uint8(api.AppVideoChange), Payload: api.AppVideoInfo{ - W: m.VideoW, - H: m.VideoH, - A: app.AspectRatio(), - S: int(app.Scale()), + W: m.VideoW, + H: m.VideoH, + A: app.AspectRatio(), + S: int(app.Scale()), + Flip: app.Flipped(), }}) if err != nil { c.log.Error().Err(err).Msgf("wrap") @@ -187,9 +188,6 @@ func (c *coordinator) HandleGameStart(rq api.StartGameRequest, w *Worker) api.Ou return api.EmptyPacket } - if app.Flipped() { - m.SetVideoFlip(true) - } m.SetPixFmt(app.PixFormat()) m.SetRot(app.Rotation()) @@ -217,7 +215,13 @@ func (c *coordinator) HandleGameStart(rq api.StartGameRequest, w *Worker) api.Ou } if r.App().AspectEnabled() { ww, hh := r.App().ViewportSize() - response.AV = &api.AppVideoInfo{W: ww, H: hh, A: r.App().AspectRatio(), S: int(r.App().Scale())} + response.AV = &api.AppVideoInfo{ + W: ww, + H: hh, + A: r.App().AspectRatio(), + S: int(r.App().Scale()), + Flip: r.App().Flipped(), + } } return api.Out{Payload: response} diff --git a/pkg/worker/media/media.go b/pkg/worker/media/media.go index 0d1407d6..1e79c272 100644 --- a/pkg/worker/media/media.go +++ b/pkg/worker/media/media.go @@ -54,9 +54,8 @@ type WebrtcMediaPipe struct { initialized bool // keep the old settings for reinit - oldPf uint32 - oldRot uint - oldFlip bool + oldPf uint32 + oldRot uint } func NewWebRtcMediaPipe(ac config.Audio, vc config.Video, log *logger.Logger) *WebrtcMediaPipe { @@ -160,13 +159,11 @@ func (wmp *WebrtcMediaPipe) Reinit() error { // restore old wmp.SetPixFmt(wmp.oldPf) wmp.SetRot(wmp.oldRot) - wmp.SetVideoFlip(wmp.oldFlip) return nil } func (wmp *WebrtcMediaPipe) IsInitialized() bool { return wmp.initialized } func (wmp *WebrtcMediaPipe) SetPixFmt(f uint32) { wmp.oldPf = f; wmp.v.SetPixFormat(f) } -func (wmp *WebrtcMediaPipe) SetVideoFlip(b bool) { wmp.oldFlip = b; wmp.v.SetFlip(b) } func (wmp *WebrtcMediaPipe) SetRot(r uint) { wmp.oldRot = r; wmp.v.SetRot(r) } func (wmp *WebrtcMediaPipe) Video() *encoder.Video { diff --git a/web/js/stream.js b/web/js/stream.js index 01718e2a..69364161 100644 --- a/web/js/stream.js +++ b/web/js/stream.js @@ -2,21 +2,21 @@ import { sub, APP_VIDEO_CHANGED, SETTINGS_CHANGED, - TRANSFORM_CHANGE -} from 'event'; -import {log} from 'log'; -import {opts, settings} from 'settings'; + TRANSFORM_CHANGE, +} from "event"; +import { log } from "log"; +import { opts, settings } from "settings"; -const videoEl = document.getElementById('stream') -const mirrorEl = document.getElementById('mirror-stream') -const playEl = document.getElementById('play-stream') +const videoEl = document.getElementById("stream"); +const mirrorEl = document.getElementById("mirror-stream"); +const playEl = document.getElementById("play-stream"); const options = { volume: 0.5, - poster: '/img/screen_loading.gif', + poster: "/img/screen_loading.gif", mirrorMode: null, mirrorUpdateRate: 1 / 60, -} +}; const state = { screen: videoEl, @@ -24,185 +24,211 @@ const state = { w: 0, h: 0, aspect: 4 / 3, - fit: 'contain', + fit: "contain", + flip: false, ready: false, - autoplayWait: false -} + autoplayWait: false, +}; -const mute = (mute) => (videoEl.muted = mute) +const flip = (flip) => (videoEl.style.transform = flip ? "scaleY(-1)" : ""); +const mute = (mute) => (videoEl.muted = mute); const onPlay = () => { - state.ready = true - videoEl.poster = '' - resize(state.w, state.h, state.aspect, state.fit) - useCustomScreen(options.mirrorMode === 'mirror') -} + state.ready = true; + videoEl.poster = ""; + flip(state.flip); + resize(state.w, state.h, state.aspect, state.fit); + useCustomScreen(options.mirrorMode === "mirror"); +}; const play = () => { - const promise = videoEl.play() + const promise = videoEl.play(); if (promise === undefined) { - log.error('oh no, the video is not a promise!') - return + log.error("oh no, the video is not a promise!"); + return; } - promise - .then(onPlay) - .catch(error => { - if (error.name === 'NotAllowedError') { - showPlayButton() - } else { - log.error('Playback fail', error) - } - }) -} + promise.then(onPlay).catch((error) => { + if (error.name === "NotAllowedError") { + showPlayButton(); + } else { + log.error("Playback fail", error); + } + }); +}; -const toggle = (show) => state.screen.toggleAttribute('hidden', show === undefined ? show : !show) +const toggle = (show) => + state.screen.toggleAttribute("hidden", show === undefined ? show : !show); const resize = (w, h, aspect, fit) => { if (!state.ready) return; - state.screen.setAttribute('width', '' + w) - state.screen.setAttribute('height', '' + h) - aspect !== undefined && (state.screen.style.aspectRatio = '' + aspect) - fit !== undefined && (state.screen.style['object-fit'] = fit) -} + state.screen.setAttribute("width", "" + w); + state.screen.setAttribute("height", "" + h); + if (aspect !== undefined) { + state.screen.style.aspectRatio = "" + aspect; + } + if (fit !== undefined) { + state.screen.style["object-fit"] = fit; + } +}; const showPlayButton = () => { - state.autoplayWait = true - toggle() - playEl.removeAttribute('hidden') -} + state.autoplayWait = true; + toggle(); + playEl.removeAttribute("hidden"); +}; -playEl.addEventListener('click', () => { - playEl.setAttribute('hidden', "") - state.autoplayWait = false - play() - toggle() -}) +playEl.addEventListener("click", () => { + playEl.setAttribute("hidden", ""); + state.autoplayWait = false; + play(); + toggle(); +}); // Track resize even when the underlying media stream changes its video size -videoEl.addEventListener('resize', () => { - recalculateSize() - if (state.screen === videoEl) return - resize(videoEl.videoWidth, videoEl.videoHeight) -}) +videoEl.addEventListener("resize", () => { + recalculateSize(); + if (state.screen === videoEl) return; + resize(videoEl.videoWidth, videoEl.videoHeight); +}); -videoEl.addEventListener('loadstart', () => { - videoEl.volume = options.volume / 100 - videoEl.poster = options.poster -}) +videoEl.addEventListener("loadstart", () => { + videoEl.volume = options.volume / 100; + videoEl.poster = options.poster; +}); -videoEl.onfocus = () => videoEl.blur() -videoEl.onerror = (e) => log.error('Playback error', e) +videoEl.onfocus = () => videoEl.blur(); +videoEl.onerror = (e) => log.error("Playback error", e); const onFullscreen = (fullscreen) => { - const el = document.fullscreenElement + const el = document.fullscreenElement; if (fullscreen) { // timeout is due to a chrome bug setTimeout(() => { // aspect ratio calc - const w = window.screen.width ?? window.innerWidth - const hh = el.innerHeight || el.clientHeight || 0 - const dw = (w - hh * state.aspect) / 2 - state.screen.style.padding = `0 ${dw}px` - state.screen.classList.toggle('with-footer') - }, 1) + const w = window.screen.width ?? window.innerWidth; + const hh = el.innerHeight || el.clientHeight || 0; + const dw = (w - hh * state.aspect) / 2; + state.screen.style.padding = `0 ${dw}px`; + state.screen.classList.toggle("with-footer"); + }, 1); } else { - state.screen.style.padding = '0' - state.screen.classList.toggle('with-footer') + state.screen.style.padding = "0"; + state.screen.classList.toggle("with-footer"); } if (el === videoEl) { - videoEl.classList.toggle('no-media-controls', !fullscreen) - videoEl.blur() + videoEl.classList.toggle("no-media-controls", !fullscreen); + videoEl.blur(); } -} +}; -const vs = {w: 1, h: 1} +const vs = { w: 1, h: 1 }; const recalculateSize = () => { - const fullscreen = document.fullscreenElement !== null - const {aspect, screen} = state + const fullscreen = document.fullscreenElement !== null; + const { aspect, screen } = state; - let width, height + let width, height; if (fullscreen) { // we can't get the real