diff --git a/Dockerfile b/Dockerfile index 7966ada37..4fa0fb85b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,5 @@ RUN apk update && apk add --no-cache ffmpeg ffmpeg-libs ca-certificates && updat WORKDIR /app COPY --from=build /build/owncast /app/owncast COPY --from=build /build/webroot /app/webroot -COPY --from=build /build/static /app/static RUN mkdir /app/data CMD ["/app/owncast"] diff --git a/build/release/build.sh b/build/release/build.sh index abec14586..a5ecc5883 100755 --- a/build/release/build.sh +++ b/build/release/build.sh @@ -67,7 +67,6 @@ build() { # Copy the production pruned+minified css to the build's directory. cp "${TMPDIR}tailwind.min.css" ./dist/${NAME}/webroot/js/web_modules/tailwindcss/dist/tailwind.min.css - cp -R static/ dist/${NAME}/static cp README.md dist/${NAME} pushd dist/${NAME} >> /dev/null diff --git a/controllers/index.go b/controllers/index.go index 7e2f84be3..3f37f1083 100644 --- a/controllers/index.go +++ b/controllers/index.go @@ -8,7 +8,6 @@ import ( "path" "path/filepath" "strings" - "text/template" log "github.com/sirupsen/logrus" @@ -17,6 +16,7 @@ import ( "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/models" "github.com/owncast/owncast/router/middleware" + "github.com/owncast/owncast/static" "github.com/owncast/owncast/utils" ) @@ -74,7 +74,12 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) { // Return a basic HTML page with server-rendered metadata from the config file // to give to Opengraph clients and web scrapers (bots, web crawlers, etc). func handleScraperMetadataPage(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles(path.Join("static", "metadata.html"))) + tmpl, err := static.GetBotMetadataTemplate() + if err != nil { + log.Errorln(err) + w.WriteHeader(http.StatusInternalServerError) + return + } scheme := "http" diff --git a/core/core.go b/core/core.go index 763da6367..08ef25591 100644 --- a/core/core.go +++ b/core/core.go @@ -1,6 +1,7 @@ package core import ( + "io" "os" "path" "path/filepath" @@ -14,6 +15,7 @@ import ( "github.com/owncast/owncast/core/transcoder" "github.com/owncast/owncast/core/user" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/static" "github.com/owncast/owncast/utils" "github.com/owncast/owncast/yp" ) @@ -79,13 +81,6 @@ func Start() error { } func createInitialOfflineState() error { - // Provide default files - if !utils.DoesFileExists(filepath.Join(config.WebRoot, "thumbnail.jpg")) { - if err := utils.Copy("static/logo.png", filepath.Join(config.WebRoot, "thumbnail.jpg")); err != nil { - return err - } - } - transitionToOfflineVideoStreamContent() return nil @@ -97,12 +92,18 @@ func createInitialOfflineState() error { func transitionToOfflineVideoStreamContent() { log.Traceln("Firing transcoder with offline stream state") - offlineFilename := "offline.ts" - offlineFilePath := "static/" + offlineFilename + r, w := io.Pipe() + _transcoder := transcoder.NewTranscoder() - _transcoder.SetInput(offlineFilePath) + _transcoder.SetInput("pipe:0") + _transcoder.SetStdin(r) _transcoder.SetIdentifier("offline") - _transcoder.Start() + go _transcoder.Start() + + d := static.GetOfflineSegment() + if _, err := w.Write(d); err != nil { + log.Errorln(err) + } // Copy the logo to be the thumbnail logo := data.GetLogoPath() diff --git a/core/streamState.go b/core/streamState.go index 9e48f3f4c..35a88b54b 100644 --- a/core/streamState.go +++ b/core/streamState.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "io" + "io/ioutil" "os" "path/filepath" "time" @@ -17,6 +18,7 @@ import ( "github.com/owncast/owncast/core/transcoder" "github.com/owncast/owncast/core/webhooks" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/static" "github.com/owncast/owncast/utils" "github.com/grafov/m3u8" @@ -84,8 +86,18 @@ func SetStreamAsDisconnected() { _stats.LastConnectTime = nil _broadcaster = nil + offlineFileData := static.GetOfflineSegment() offlineFilename := "offline.ts" - offlineFilePath := "static/" + offlineFilename + offlineTmpFile, err := ioutil.TempFile(os.TempDir(), offlineFilename) + if err != nil { + log.Errorln("unable to create temp file for offline video segment") + } + + if _, err = offlineTmpFile.Write(offlineFileData); err != nil { + log.Errorln("unable to write offline segment to disk", err) + } + + offlineFilePath := offlineTmpFile.Name() transcoder.StopThumbnailGenerator() rtmp.Disconnect() diff --git a/core/transcoder/transcoder.go b/core/transcoder/transcoder.go index a47bd217d..229bddd9c 100644 --- a/core/transcoder/transcoder.go +++ b/core/transcoder/transcoder.go @@ -386,8 +386,8 @@ func (t *Transcoder) SetInput(input string) { } // SetStdin sets the Stdin of the ffmpeg command. -func (t *Transcoder) SetStdin(rtmp *io.PipeReader) { - t.stdin = rtmp +func (t *Transcoder) SetStdin(pipe *io.PipeReader) { + t.stdin = pipe } // SetOutputPath sets the root directory that should include playlists and video segments. diff --git a/static/logo.png b/static/logo.png deleted file mode 100644 index 4dd93a711..000000000 Binary files a/static/logo.png and /dev/null differ diff --git a/static/metadata.html b/static/metadata.html.tmpl similarity index 100% rename from static/metadata.html rename to static/metadata.html.tmpl diff --git a/static/static.go b/static/static.go index e64ca5f14..a530d3e82 100644 --- a/static/static.go +++ b/static/static.go @@ -1,6 +1,9 @@ package static -import "embed" +import ( + "embed" + "html/template" +) //go:embed admin/* //go:embed admin/_next/static @@ -12,3 +15,22 @@ var adminFiles embed.FS func GetAdmin() embed.FS { return adminFiles } + +//go:embed metadata.html.tmpl +var botMetadataTemplate embed.FS + +// GetBotMetadataTemplate will return the bot/scraper metadata template. +func GetBotMetadataTemplate() (*template.Template, error) { + name := "metadata.html.tmpl" + t, err := template.ParseFS(botMetadataTemplate, name) + tmpl := template.Must(t, err) + return tmpl, err +} + +//go:embed offline.ts +var offlineVideoSegment []byte + +// GetOfflineSegment will return the offline video segment data. +func GetOfflineSegment() []byte { + return offlineVideoSegment +} diff --git a/test/automated/browser/bot-share-search-scrapers.test.js b/test/automated/browser/bot-share-search-scrapers.test.js new file mode 100644 index 000000000..35e6112b8 --- /dev/null +++ b/test/automated/browser/bot-share-search-scrapers.test.js @@ -0,0 +1,48 @@ +const listenForErrors = require('./lib/errors.js').listenForErrors; + +describe('Video embed page', () => { + + async function getMetaTagContent(property) { + const selector = `meta[property="${property}"]`; + + const tag = await page.evaluate((selector) => { + return document.head.querySelector(selector).getAttribute("content"); + }, selector); + return tag; + } + + beforeAll(async () => { + await page.setViewport({ width: 1080, height: 720 }); + listenForErrors(browser, page); + page.setUserAgent( + "Mastodon" + ); + await page.goto('http://localhost:5309'); + }); + + afterAll(async () => { + await page.waitForTimeout(3000); + await page.screenshot({ path: 'screenshots/screenshot_bots_share_search_scrapers.png', fullPage: true }); + }); + + it('should have rendered the simple bot accessible html page', async () => { + await page.waitForSelector('h1'); + await page.waitForSelector('h3'); + + const ogVideo = await getMetaTagContent('og:video'); + expect(ogVideo).toBe('http://localhost:5309/hls/stream.m3u8'); + + const ogVideoType = await getMetaTagContent('og:video:type'); + expect(ogVideoType).toBe('application/x-mpegURL'); + + // When stream is live the thumbnail is provided as the image. + const ogImage = await getMetaTagContent('og:image'); + expect(ogImage).toBe('http://localhost:5309/thumbnail.jpg'); + + const twitterUrl = await getMetaTagContent('twitter:url'); + expect(twitterUrl).toBe('http://localhost:5309/'); + + const twitterImage = await getMetaTagContent('twitter:image'); + expect(twitterImage).toBe('http://localhost:5309/logo/external'); + }); +});