diff --git a/Dockerfile b/Dockerfile index 6e4b210..db6d6e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,77 @@ FROM golang:1.13.6-alpine3.11 as builder RUN apk add --no-cache curl +# ffmpeg source - https://github.com/alfg/docker-ffmpeg +ARG FFMPEG_VERSION=4.2.2 +ARG PREFIX=/opt/ffmpeg +ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib +ARG MAKEFLAGS="-j4" + +# FFmpeg build dependencies. +RUN apk add --update \ + build-base \ + coreutils \ + freetype-dev \ + gcc \ + lame-dev \ + libogg-dev \ + libass \ + libass-dev \ + libvpx-dev \ + libvorbis-dev \ + libwebp-dev \ + libtheora-dev \ + opus-dev \ + pkgconf \ + pkgconfig \ + rtmpdump-dev \ + wget \ + x264-dev \ + x265-dev \ + yasm + +# Get fdk-aac from testing. +RUN echo http://dl-cdn.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories && \ + apk add --update fdk-aac-dev + +# Get ffmpeg source. +RUN cd /tmp/ && \ + wget http://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz && \ + tar zxf ffmpeg-${FFMPEG_VERSION}.tar.gz && rm ffmpeg-${FFMPEG_VERSION}.tar.gz + +# Compile ffmpeg. +RUN cd /tmp/ffmpeg-${FFMPEG_VERSION} && \ + ./configure \ + --enable-version3 \ + --enable-gpl \ + --enable-nonfree \ + --enable-small \ + --enable-libmp3lame \ + --enable-libx264 \ + --enable-libx265 \ + --enable-libvpx \ + --enable-libtheora \ + --enable-libvorbis \ + --enable-libopus \ + --enable-libfdk-aac \ + --enable-libass \ + --enable-libwebp \ + --enable-librtmp \ + --enable-postproc \ + --enable-avresample \ + --enable-libfreetype \ + --disable-debug \ + --disable-doc \ + --disable-ffplay \ + --extra-cflags="-I${PREFIX}/include" \ + --extra-ldflags="-L${PREFIX}/lib" \ + --extra-libs="-lpthread -lm" \ + --prefix="${PREFIX}" && \ + make && make install && make distclean + +# Cleanup. +RUN rm -rf /var/cache/apk/* /tmp/* + WORKDIR /app COPY src src @@ -13,17 +84,36 @@ RUN go build -x -o media-roller ./src # youtube-dl needs python FROM python:3.8.1-alpine3.11 -RUN apk add --no-cache ffmpeg \ - curl && \ - ffmpeg -version + +ENV PATH=/opt/ffmpeg/bin:$PATH + +RUN apk add --update --no-cache \ + curl \ + ca-certificates \ + openssl \ + pcre \ + lame \ + libogg \ + libass \ + libvpx \ + libvorbis \ + libwebp \ + libtheora \ + opus \ + rtmpdump \ + x264-dev \ + x265-dev COPY --from=builder /app/media-roller /app/media-roller +COPY --from=builder /opt/ffmpeg /opt/ffmpeg +COPY --from=builder /usr/lib/libfdk-aac.so.2 /usr/lib/libfdk-aac.so.2 COPY templates /app/templates WORKDIR /app RUN curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl && \ chmod a+rx /usr/local/bin/youtube-dl && \ - youtube-dl --version + youtube-dl --version && \ + ffmpeg -version CMD /app/media-roller diff --git a/src/media/fetch.go b/src/media/fetch.go index 1bc3f73..b01d507 100644 --- a/src/media/fetch.go +++ b/src/media/fetch.go @@ -25,6 +25,7 @@ type ResponseData struct { Id string } +// TODO: Use something better than this. It's too tedious to map var fetchResponseTmpl = template.Must(template.ParseFiles("templates/media/response.html")) var fetchIndexTmpl = template.Must(template.ParseFiles("templates/media/index.html")) @@ -59,14 +60,19 @@ func FetchMedia(w http.ResponseWriter, r *http.Request) { // returns the ID of the file func fetch(url string) (string, error) { - // This will be the output file name + // The id will be used as the name of the parent directory of the output files id := uuid.New().String() - // youtube-dl will add the extension as needed - name := getFilenameWithoutExtensionById(id) + name := getMediaDirectory(id) + "%(title)s.%(ext)s" log.Info().Msgf("Downloading %s to %s", url, id) - cmd := exec.Command("youtube-dl", "-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4/", "-o", name, url) + cmd := exec.Command("youtube-dl", + "--format", "bestvideo+bestaudio[ext=m4a]/bestvideo+bestaudio/best", + "--merge-output-format", "mp4", + "--restrict-filenames", + "--write-info-json", + "--output", name, + url) var stdoutBuf, stderrBuf bytes.Buffer stdoutIn, _ := cmd.StdoutPipe() @@ -107,13 +113,6 @@ func fetch(url string) (string, error) { return id, nil } -// Returns the relative filename without the extension. Example: -// downloads/b541cc43-9833-4146-ab19-71334484c0c1/media -// where media can be media.mp4 -func getFilenameWithoutExtensionById(id string) string { - return getMediaDirectory(id) + "media" -} - // Returns the relative directory containing the media file, with a trailing slash // Id is expected to be pre validated func getMediaDirectory(id string) string { diff --git a/src/media/serve.go b/src/media/serve.go index c3bb7c8..b8f83ed 100644 --- a/src/media/serve.go +++ b/src/media/serve.go @@ -3,6 +3,8 @@ package media import ( "github.com/rs/zerolog/log" "net/http" + "path/filepath" + "strings" ) /** @@ -47,12 +49,20 @@ func getFileFromId(id string) (string, error) { files, _ := file.Readdirnames(0) // 0 to read all files and folders if len(files) == 0 { return "", errors.New("ID not found") - } else if len(files) > 1 { - // We should only have 1 media file produced + } else if len(files) > 2 { + // We should only have 2 media file produced, the mp4 and the json file return "", errors.New("internal error") } - return root + files[0], nil + // We expect two files to be produced, a json manifest and an mp4. We want to return the mp4 + // Sometimes the video file might not have an mp4 extension, so filter out the json file + for _, f := range files { + if !strings.HasSuffix(f, ".json") { + return root + f, nil + } + } + + return "", errors.New("unable to find file") } func streamFileToClient(writer http.ResponseWriter, filename string) { @@ -83,8 +93,7 @@ func streamFileToClient(writer http.ResponseWriter, filename string) { fileSize := strconv.FormatInt(fileStat.Size(), 10) // Send the headers - // Set the following if you want to force the client to download the file - // writer.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename)) + writer.Header().Set("Content-Disposition", "filename="+filepath.Base(filename)) writer.Header().Set("Content-Type", fileContentType) writer.Header().Set("Content-Length", fileSize)