From 5b8a63c0aad6504887d430844aa227be552b940a Mon Sep 17 00:00:00 2001 From: lukas-heiligenbrunner Date: Sun, 25 Dec 2022 23:19:04 +0100 Subject: [PATCH] create on the fly hls livestream when video is started and stream it to client --- apiGo/api/Video.go | 59 ++++++++++++ apiGo/config/Config.go | 2 + apiGo/videoparser/hls/HlsDecoding.go | 133 +++++++++++++++++++++++++++ package.json | 3 +- src/pages/Player/HLSPlayer.tsx | 34 +++++++ src/pages/Player/Player.tsx | 12 +-- yarn.lock | 5 + 7 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 apiGo/videoparser/hls/HlsDecoding.go create mode 100644 src/pages/Player/HLSPlayer.tsx diff --git a/apiGo/api/Video.go b/apiGo/api/Video.go index c9c2d2e..71b7fb8 100644 --- a/apiGo/api/Video.go +++ b/apiGo/api/Video.go @@ -3,12 +3,15 @@ package api import ( "encoding/json" "fmt" + "net/http" "net/url" "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/api/types" "openmediacenter/apiGo/config" "openmediacenter/apiGo/database" + "openmediacenter/apiGo/videoparser/hls" "os" + "os/exec" "strconv" "strings" ) @@ -398,6 +401,62 @@ func loadVideosHandlers() { context.Json(result) }) + + api.AddHandler("loadM3U8", api.VideoNode, api.PermUnauthorized, func(ctx api.Context) { + param := ctx.GetRequest().URL.Query().Get("id") + id, _ := strconv.Atoi(param) + + mylist := + `#EXTM3U +#EXT-X-VERSION:4 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-ALLOW-CACHE:NO +#EXT-X-TARGETDURATION:10 +#EXT-X-START:TIME-OFFSET=0 +#EXT-X-PLAYLIST-TYPE:VOD +` + // ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4 + cmd := exec.Command("ffprobe", + "-v", "error", + "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", + hls.GetVideoPathById(uint32(id))) + stdout, err := cmd.Output() + // + if err != nil { + fmt.Println(err.Error()) + fmt.Println(string(err.(*exec.ExitError).Stderr)) + } + secss, _, _ := strings.Cut(string(stdout), ".") + secsi, err := strconv.Atoi(secss) + + i := 0 + for ; i < secsi/10; i++ { + mylist += fmt.Sprintf( + `#EXTINF:10.0, +/api/video/getVideoSegment?id=%d&idx=%d +`, id, i) + } + mylist += fmt.Sprintf( + `#EXTINF:%s, +/api/video/getVideoSegment?id=%d&idx=%d +EXT-X-ENDLIST +`, fmt.Sprintf("%d.0", secsi%10), id, i) + ctx.Text(mylist) + }) + + api.AddHandler("getVideoSegment", api.VideoNode, api.PermUnauthorized, func(ctx api.Context) { + params := ctx.GetRequest().URL.Query() + idxs := params.Get("idx") + ids := params.Get("id") + + id, _ := strconv.Atoi(ids) + idx, _ := strconv.Atoi(idxs) + // todo error handling + + tmppath := hls.GetSegment(uint32(idx), uint32(id)) + http.ServeFile(ctx.GetWriter(), ctx.GetRequest(), tmppath) + }) } func addToVideoHandlers() { diff --git a/apiGo/config/Config.go b/apiGo/config/Config.go index a8f292e..2250ee2 100644 --- a/apiGo/config/Config.go +++ b/apiGo/config/Config.go @@ -24,6 +24,7 @@ type FeaturesT struct { type GeneralT struct { VerboseLogging bool ReindexPrefix string + TmpDir string } type FileConfT struct { @@ -44,6 +45,7 @@ func defaultConfig() *FileConfT { General: GeneralT{ VerboseLogging: false, ReindexPrefix: "/var/www/openmediacenter", + TmpDir: "/tmp/openmediacenter", }, Features: FeaturesT{ DisableTVSupport: false, diff --git a/apiGo/videoparser/hls/HlsDecoding.go b/apiGo/videoparser/hls/HlsDecoding.go new file mode 100644 index 0000000..00a46fe --- /dev/null +++ b/apiGo/videoparser/hls/HlsDecoding.go @@ -0,0 +1,133 @@ +package hls + +import ( + "fmt" + "log" + "openmediacenter/apiGo/config" + "openmediacenter/apiGo/database" + "os" + "os/exec" + "time" +) + +type TranscodingState struct { + Finished bool + Active bool +} + +var transcodeAcive = make(map[uint32]TranscodingState) + +func startSegmentation(videoid uint32) { + transcodeAcive[videoid] = TranscodingState{ + Finished: false, + Active: true, + } + + cfg := config.GetConfig() + outpath := fmt.Sprintf("%s/%d", cfg.General.TmpDir, videoid) + + if !fileExists(outpath) { + err := os.MkdirAll(outpath, os.ModePerm) + if err != nil { + log.Println(err) + } + } + + app := "ffmpeg" + inputpath := GetVideoPathById(videoid) + fmt.Println(inputpath) + cmd := exec.Command(app, + //"-hide_banner", + //"-loglevel", "panic", + "-n", + //"-t", "10.0", + //"-ss", fmt.Sprintf("%d", id*10), + "-i", inputpath, + //"-g", "52", + //"-sc_threshold", "0", + //"-force_key_frames", "expr:gte(t,n_forced*10)", + //"-strict", "experimental", + //"-movflags", "+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov", + //"-c:v", "libx264", + //"-crf", "18", + // + //"-f", "segment", + //"-segment_time_delta", "0.2", + //"-segment_format", "mpegts", + //"-segment_times", commaSeparatedTimes, + //"-segment_start_number", `0`, + //"-segment_list_type", "flat", + //"-segment_list", + "-preset", "veryfast", + //"-maxrate", "4000k", + //"-bufsize", "8000k", + //"-vf", "scale=1280:-1,format=yuv420p", + //"-c:a", "aac", + "-force_key_frames", "expr:gte(t,n_forced*10)", + "-strict", "-2", + "-c:a", "aac", + "-c:v", "libx264", + "-f", "segment", + "-segment_list_type", "m3u8", + "-segment_time", "10.0", + "-segment_time_delta", "0.001", + "-segment_list", "test.m3u8", outpath+"/part%02d.ts") + + stdout, err := cmd.Output() + // + if err != nil { + fmt.Println(err.Error()) + fmt.Println(string(err.(*exec.ExitError).Stderr)) + } + fmt.Println(stdout) + fmt.Println("finished transcoding") + + transcodeAcive[videoid] = TranscodingState{ + Finished: true, + Active: false, + } +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} + +func GetVideoPathById(videoid uint32) string { + query := fmt.Sprintf(`SELECT movie_url FROM videos WHERE movie_id=%d`, videoid) + var url string + + err := database.QueryRow(query).Scan(&url) + if err != nil { + return "" + } + + mSettings, _, _ := database.GetSettings() + vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath + return vidFolder + url +} + +func GetSegment(segIdx uint32, videoid uint32) string { + cfg := config.GetConfig() + + i, ok := transcodeAcive[videoid] + if ok { + if !i.Active && !i.Finished { + go startSegmentation(videoid) + } + } else { + go startSegmentation(videoid) + } + + // todo timeout + tspath := fmt.Sprintf("%s/%d/part%02d.ts", cfg.General.TmpDir, videoid, segIdx) + if ok && i.Finished == true { + return tspath + } + + fmt.Println("checking if part exists") + for !fileExists(fmt.Sprintf("%s/%d/part%02d.ts", cfg.General.TmpDir, videoid, segIdx+1)) { + time.Sleep(100 * time.Millisecond) + } + return tspath +} diff --git a/package.json b/package.json index 1890f28..a430aea 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/react-fontawesome": "^0.1.13", "bootstrap": "^5.0.2", + "hls.js": "^1.2.9", "plyr-react": "^3.0.7", "react": "^17.0.1", "react-bootstrap": "^1.4.0", @@ -22,7 +23,7 @@ "typescript": "^4.3.5" }, "scripts": { - "start": "react-scripts start", + "start": "react-scripts --openssl-legacy-provider start", "build": "CI=false react-scripts build", "test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default", "lint": "eslint --format gitlab src/", diff --git a/src/pages/Player/HLSPlayer.tsx b/src/pages/Player/HLSPlayer.tsx new file mode 100644 index 0000000..39a8b4a --- /dev/null +++ b/src/pages/Player/HLSPlayer.tsx @@ -0,0 +1,34 @@ +import React, {useEffect, useRef} from 'react'; +import Hls from 'hls.js'; +import {PlyrInstance, PlyrProps, Plyr} from 'plyr-react'; +import plyrstyle from 'plyr-react/dist/plyr.css'; +import {DefaultPlyrOptions} from '../../types/GeneralTypes'; + +interface Props { + // children?: JSX.Element; + videoid: number; +} + +const HLSPlayer = (props: Props): JSX.Element => { + const ref = useRef(null); + useEffect(() => { + const loadVideo = async (): Promise => { + const video = document.getElementById('plyr') as HTMLVideoElement; + const hls = new Hls(); + hls.loadSource('/api/video/loadM3U8?id=' + props.videoid); + hls.attachMedia(video); + // @ts-ignore + ref.current!.plyr.media = video; + + hls.on(Hls.Events.MANIFEST_PARSED, function () { + // @ts-ignore + (ref.current!.plyr as PlyrInstance).play(); + }); + }; + loadVideo(); + }); + + return ; +}; + +export default HLSPlayer; diff --git a/src/pages/Player/Player.tsx b/src/pages/Player/Player.tsx index c14bb38..991c969 100644 --- a/src/pages/Player/Player.tsx +++ b/src/pages/Player/Player.tsx @@ -1,9 +1,7 @@ import React from 'react'; import style from './Player.module.css'; -import plyrstyle from 'plyr-react/dist/plyr.css'; -import {Plyr} from 'plyr-react'; import SideBar, {SideBarItem, SideBarTitle} from '../../elements/SideBar/SideBar'; import Tag from '../../elements/Tag/Tag'; import AddTagPopup from '../../elements/Popups/AddTagPopup/AddTagPopup'; @@ -15,7 +13,7 @@ import ActorTile from '../../elements/ActorTile/ActorTile'; import {withRouter} from 'react-router-dom'; import {APINode, callAPI} from '../../utils/Api'; import {RouteComponentProps} from 'react-router'; -import {DefaultPlyrOptions, GeneralSuccess} from '../../types/GeneralTypes'; +import {GeneralSuccess} from '../../types/GeneralTypes'; import {ActorType, TagType} from '../../types/VideoTypes'; import PlyrJS from 'plyr'; import {IconButton} from '../../elements/GPElements/Button'; @@ -23,6 +21,7 @@ import {VideoTypes} from '../../types/ApiTypes'; import GlobalInfos from '../../utils/GlobalInfos'; import {ButtonPopup} from '../../elements/Popups/ButtonPopup/ButtonPopup'; import {FeatureContext} from '../../utils/context/FeatureContext'; +import HLSPlayer from './HLSPlayer'; interface Props extends RouteComponentProps<{id: string}> {} @@ -84,11 +83,8 @@ export class Player extends React.Component {
{/* video component is added here */} - {this.state.sources ? ( - - ) : ( -
not loaded yet
- )} + {/**/} + {this.state.sources ? :
not loaded yet
}
this.likebtn()} title='Like!' /> this.setState({popupvisible: true})} title='Add Tag!' /> diff --git a/yarn.lock b/yarn.lock index 5371700..bdf0e34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6191,6 +6191,11 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" +hls.js@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.2.9.tgz#2f25e42ec4c2ea8c88ab23c0f854f39062d45ac9" + integrity sha512-SPjm8ix0xe6cYzwDvdVGh2QvQPDkCYrGWpZu6bRaKNNVyEGWM9uF0pooh/Lqj/g8QBQgPFEx1vHzW8SyMY9rqg== + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"