Skip to content

Commit

Permalink
bugfix: Fix hls bandwidth issue (#18)
Browse files Browse the repository at this point in the history
* Began changing ffmpeg to transcode

* Finished replacing ffmpeg command

* Cleaned up thumbnail uploads

* Cleaned up ffmpeg command building
  • Loading branch information
neboman11 authored Aug 27, 2021
1 parent 1c12c5f commit e5288e5
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 113 deletions.
177 changes: 104 additions & 73 deletions ffmpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package main
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"os"
"os/exec"
Expand Down Expand Up @@ -36,10 +36,22 @@ const HLSChunkLength = 10

// Standard video resolutions for transcoding videos
// These are the widths associated with the standard 16:9 resolutions
var videoResolutions = []int64{426, 640, 854, 1280, 1920}

// These are the standard heights for 16:9 videos
var videoResolutionsStr = []string{"240", "360", "480", "720", "1080"}
var standardVideoWidths = []int64{426, 640, 854, 1280, 1920}
var standardVideoHeigths = []int64{240, 360, 480, 720, 1080}
var resolutionBitRates = map[int]string{
240: "500k",
360: "1M",
480: "2M",
720: "3M",
1080: "5M",
}
var resolutionBufferSizes = map[int]string{
240: "1M",
360: "2M",
480: "4M",
720: "6M",
1080: "10M",
}

// Videos Currently being processed
var encodingVideos EncodingVideos
Expand Down Expand Up @@ -90,92 +102,115 @@ func getVideoResolution(videoFile string) (string, error) {
return strings.TrimSpace(string(out)), nil
}

// Builds the array of arguments necessary for ffmpeg to properly transcode the given video
// This downscales the video to the maximum and below of a horizontal width of 1920, 1280, 854, 640, 426
func buildFfmpegCommand(videoFile, videoFolder string) ([]string, int, error) {
// Initial arguments for formatting ffmpeg's output
ffmpegArgs := []string{"-i", videoFile, "-loglevel", "error", "-progress", "-", "-nostats"}
// FFMPEG command building

// Get the resolution of the current video
videoResolution, err := getVideoResolution(videoFile)
if err != nil {
return nil, 0, err
}
func buildFfmpegFilter(numResolutions int) []string {
ffmpegFilter := []string{"-filter_complex"}
filterString := fmt.Sprintf("[0:v]split=%d", numResolutions)

videoWidth, err := strconv.ParseInt(strings.Split(videoResolution, "x")[0], 10, 64)
if err != nil {
return nil, 0, err
// Split the video into numResolutions parts
for i := 0; i < numResolutions; i++ {
filterString += fmt.Sprintf("[v%d]", i+1)
}
videoHeight, err := strconv.ParseInt(strings.Split(videoResolution, "x")[1], 10, 64)
if err != nil {
return nil, 0, err

filterString += "; "

// Scale each stream to the appropriate resolution
for i := 0; i < numResolutions; i++ {
filterString += fmt.Sprintf("[v%d]scale=h=%d:-2[v%dout]", i+1, standardVideoHeigths[i], i+1)
if (i + 1) < numResolutions {
filterString += "; "
}
}

// Find its aspect ratio for calculating the scaled resolutions
aspectRatio := float64(videoHeight) / float64(videoWidth)
ffmpegFilter = append(ffmpegFilter, filterString)

// Find the maximum resolution to scale the video to
maxResolutionIndex := 0
for ; maxResolutionIndex < len(videoResolutions)-1 && videoWidth > int64(videoResolutions[maxResolutionIndex]); maxResolutionIndex++ {
return ffmpegFilter
}

func buildFfmpegVideoStreamParams(numResolutions int) []string {
ffmpegVideoStreamParams := []string{}

for i := 0; i < numResolutions; i++ {
ffmpegVideoStreamParams = append(ffmpegVideoStreamParams, "-map", fmt.Sprintf("[v%dout]", i+1), fmt.Sprintf("-c:v:%d", i), "libx264", fmt.Sprintf("-b:v:%d", i), resolutionBitRates[int(standardVideoHeigths[i])], fmt.Sprintf("-maxrate:v:%d", i), resolutionBitRates[int(standardVideoHeigths[i])], fmt.Sprintf("-minrate:v:%d", i), resolutionBitRates[int(standardVideoHeigths[i])], fmt.Sprintf("-bufsize:v:%d", i), resolutionBufferSizes[int(standardVideoHeigths[i])], "-preset", "ultrafast", "-crf", "23", "-g", "48", "-sc_threshold", "0", "-keyint_min", "48")
}

// Include the current resolution if the resolution matches
if videoWidth == int64(videoResolutions[maxResolutionIndex]) {
maxResolutionIndex++
return ffmpegVideoStreamParams
}

// -map a:0 -c:a:0 aac -b:a:0 96k -ac 2
func buildFfmpegAudioStreamParams(numResolutions int) []string {
ffmpegAudioStreamParams := []string{}

for i := 0; i < numResolutions; i++ {
ffmpegAudioStreamParams = append(ffmpegAudioStreamParams, "-map", "a:0", fmt.Sprintf("-c:a:%d", i), "aac" /*fmt.Sprintf("-b:a:%d", i), "96k",*/, "-ac", "2")
}

outputResolutions := videoResolutions[0:maxResolutionIndex]
numResolutions := len(outputResolutions)
return ffmpegAudioStreamParams
}

func buildFfmpegHLSParams(videoFolder string) []string {
ffmpegHLSParams := []string{"-f", "hls", "-hls_time", "2", "-hls_playlist_type", "vod", "-hls_flags", "independent_segments", "-hls_segment_type", "mpegts", "-hls_segment_filename", path.Join(videoFolder, "stream_%v-data%02d.ts"), "-master_pl_name", "master.m3u8"}
return ffmpegHLSParams
}

func buildFfmpegVarStreamMapParams(numResolutions int) []string {
ffmpegVarStreamMapParams := []string{"-var_stream_map"}
streamMap := ""

// Add a series of parameters for each resolution
for i := 0; i < numResolutions; i++ {
resolutionHeight := int64(math.Floor(float64(videoResolutions[i]) * aspectRatio))
if resolutionHeight%2 != 0 {
resolutionHeight++
streamMap += fmt.Sprintf("v:%d,a:%d", i, i)
if (i + 1) < numResolutions {
streamMap += " "
}

ffmpegArgs = append(ffmpegArgs, "-s", strconv.FormatInt(videoResolutions[i], 10)+"x"+strconv.FormatInt(resolutionHeight, 10), "-hls_playlist_type", "vod", "-hls_flags", "independent_segments", "-hls_segment_type", "mpegts", "-hls_segment_filename", path.Join(videoFolder, "stream_"+videoResolutionsStr[i]+"_data%02d.ts"), "-hls_time", "10", "-master_pl_name", "master"+videoResolutionsStr[i]+".m3u8", "-f", "hls", path.Join(videoFolder, "stream_"+videoResolutionsStr[i]+".m3u8"))
}

return ffmpegArgs, maxResolutionIndex, nil
ffmpegVarStreamMapParams = append(ffmpegVarStreamMapParams, streamMap)

return ffmpegVarStreamMapParams
}

// Combines the master playlists generated by ffmpeg into a single master playlist
func combineMasterPlaylists(videoFolder string, maxResolutionIndex int) error {
// Create the master playlist file
destination, err := os.Create(path.Join(videoFolder, "master.m3u8"))
// Builds the array of arguments necessary for ffmpeg to properly transcode the given video
func buildFfmpegCommand(videoFile, videoFolder string) ([]string, error) {
// Initial arguments for formatting ffmpeg's output
ffmpegArgs := []string{"-i", videoFile, "-loglevel", "error", "-progress", "-", "-nostats"}

maxResolutionIndex, err := determineMaxResolutionIndex(videoFile, videoFolder)
if err != nil {
return err
return nil, err
}
defer destination.Close()

// Copy the important parts of each resolution's playlist file to the master
for i := 0; i < maxResolutionIndex; i++ {
file, err := ioutil.ReadFile(path.Join(videoFolder, "master"+videoResolutionsStr[i]+".m3u8"))
if err != nil {
return err
}
outputResolutions := standardVideoWidths[0:maxResolutionIndex]
numResolutions := len(outputResolutions)

// Write the entire first file to the master to get the necesssary headers
if i == 0 {
destination.Write(file)
} else {
lines := strings.Split(string(file), "\n")

// Currently the way ffmpeg creates these files, the third and fourth lines contain the data specific to the stream in question
destination.WriteString(lines[2])
destination.WriteString("\n")
destination.WriteString(lines[3])
destination.WriteString("\n")
}
ffmpegArgs = append(ffmpegArgs, buildFfmpegFilter(numResolutions)...)
ffmpegArgs = append(ffmpegArgs, buildFfmpegVideoStreamParams(numResolutions)...)
ffmpegArgs = append(ffmpegArgs, buildFfmpegAudioStreamParams(numResolutions)...)
ffmpegArgs = append(ffmpegArgs, buildFfmpegHLSParams(videoFolder)...)
ffmpegArgs = append(ffmpegArgs, buildFfmpegVarStreamMapParams(numResolutions)...)
ffmpegArgs = append(ffmpegArgs, path.Join(videoFolder, "stream_%v.m3u8"))

return ffmpegArgs, nil
}

func determineMaxResolutionIndex(videoFile, videoFolder string) (int, error) {
// Get the resolution of the current video
videoResolution, err := getVideoResolution(videoFile)
if err != nil {
return 0, err
}

// Delete the file after it has been added to the master
os.Remove(path.Join(videoFolder, "master"+videoResolutionsStr[i]+".m3u8"))
videoWidth, err := strconv.ParseInt(strings.Split(videoResolution, "x")[0], 10, 64)
if err != nil {
return 0, err
}

destination.WriteString("\n")
// Find the maximum resolution to scale the video to
maxResolutionIndex := 0
for ; maxResolutionIndex < len(standardVideoWidths)-1 && videoWidth > int64(standardVideoWidths[maxResolutionIndex]); maxResolutionIndex++ {
}

return nil
return maxResolutionIndex, nil
}

// Converts the given video to HLS chunks and places them in a folder named with the video's UUID
Expand All @@ -188,10 +223,11 @@ func convertToHLS(videoFile, videoUUID string) (videoFolder string, err error) {
}

// Build the ffmpeg command that transcodes the given video to multiple HLS streams of different resolutions
ffmpegArgs, maxResolutionIndex, err := buildFfmpegCommand(videoFile, videoFolder)
ffmpegArgs, err := buildFfmpegCommand(videoFile, videoFolder)
if err != nil {
return "", errors.New("Failed to build ffmpeg command: " + err.Error())
}
log.Debug().Msg(strings.Join(ffmpegArgs, " "))

// Convert video
cmd := exec.Command(viper.GetString("ffmpeg.ffmpegDir"), ffmpegArgs...)
Expand Down Expand Up @@ -219,11 +255,6 @@ func convertToHLS(videoFile, videoUUID string) (videoFolder string, err error) {
return "", err
}

err = combineMasterPlaylists(videoFolder, maxResolutionIndex)
if err != nil {
return "", err
}

return videoFolder, nil
}

Expand All @@ -250,7 +281,7 @@ func updateEncodeFrameProgress(ffmpegStdOut io.ReadCloser, videoUUID string) {
}

// Take the frame count out of the output of ffmpeg
output := string(buf)
output := strings.Trim(string(buf), "\u0000")
log.Debug().Str("ffmpeg", "stdout").Msg(output)
frameLine := strings.Split(output, "\n")[0]
frameCountStr := strings.Split(frameLine, "=")[1]
Expand Down
46 changes: 6 additions & 40 deletions restAPI.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/http"
"os"
"path"
"path/filepath"
"strings"

"github.com/google/uuid"
Expand Down Expand Up @@ -169,6 +168,9 @@ func uploadVideo(w http.ResponseWriter, r *http.Request) {
return
}

// Remove scratch thumbnail file
os.Remove(thumbnailFilename)

json.NewEncoder(w).Encode(VideoStartEncodingResponse{ID: videoUUID, ThumbnailCID: thumbnailCID})

log.Trace().Msgf("Finished video pre-processing. Starting encoding of %s", videoFilename)
Expand Down Expand Up @@ -214,6 +216,9 @@ func uploadThumbnail(w http.ResponseWriter, r *http.Request) {
return
}

// Remove scratch thumbnail file
os.Remove(thumbnailFilename)

json.NewEncoder(w).Encode(ThumbnailUploadResponse{CID: thumbnailCID})
}

Expand Down Expand Up @@ -258,16 +263,6 @@ func asyncVideoUpload(video, thumbnail, videoUUID string) {
// Remove scratch video file
os.Remove(video)

// Copy the thumbnail into the transcoded video folder
thumbnailFileExtension := filepath.Ext(thumbnail)
if err = fileCopy(thumbnail, path.Join(videoFolder, "thumbnail"+thumbnailFileExtension)); err != nil {
log.Error().Msgf("Unable to copy thumbnail file: %s\n", err)
return
}

// Remove scratch thumbnail file
os.Remove(thumbnail)

// Add video folder to IPFS
videoCID, err := addFolderToIPFS(ctx, videoFolder)
if err != nil {
Expand Down Expand Up @@ -303,32 +298,3 @@ func writeMultiPartFormDataToDisk(multipartFormData io.ReadCloser, destFile stri

return nil
}

// Simple file copy function
func fileCopy(src, dst string) error {
sourceFileStat, err := os.Stat(src)
if err != nil {
return err
}

if !sourceFileStat.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", src)
}

source, err := os.Open(src)
if err != nil {
return err
}
defer source.Close()

destination, err := os.Create(dst)
if err != nil {
return err
}
defer destination.Close()
_, err = io.Copy(destination, source)
if err != nil {
return err
}
return nil
}

0 comments on commit e5288e5

Please sign in to comment.