Skip to content

Commit

Permalink
stream ffmpeg output to app (#1)
Browse files Browse the repository at this point in the history
* stream ffmpeg output to app

* stream ffmpeg output to frontend

* overflow-x hidden

* move buttons up\nmodularise and update readme
  • Loading branch information
Inveracity authored Nov 8, 2022
1 parent 206203c commit 1d66b7e
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 32 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

> ⚠️ Work In Progress
![](https://i.imgur.com/gIhwTro.gif)
![](https://i.imgur.com/NzPQe0o.gif)

The gif above shows a video file where the peak volume is -7.9dB, and after normalization it's raised to -1.1dB peak volume.

## Quickstart (Windows)

Install ffmpeg
Install ffmpeg

```powershell
choco install ffmpg
Expand All @@ -33,6 +33,7 @@ Features:
Goals:
- Replace the audio track of a video with another audio file
- Configurable path to ffmpeg binary
- Cancel a normalization

## Development

Expand Down
2 changes: 1 addition & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func NewApp() *App {
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
a.v = volume.NewVolume()
a.v = volume.NewVolume(a.ctx)
}

// Get path to video file
Expand Down
92 changes: 82 additions & 10 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
import { useState } from 'react';
import { Normalize, Videofile, Volume } from "../wailsjs/go/main/App";
import { Box, Button, Stack, Text, Image, Progress, Divider } from "@chakra-ui/react"
import { Box, Button, Stack, Text, Image, Progress, Divider, Collapse } from "@chakra-ui/react"
import banner from "./assets/banner.png"

import { EventsOff, EventsOn } from "../wailsjs/runtime"

function App() {
const [maxVolume, setMaxVolume] = useState('');
const [meanVolume, setMeanVolume] = useState('');
const [file, setFile] = useState('');
const [normalizedFile, setNormalized] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [stream, setStream] = useState("");
const [stdout, setStdout] = useState([""]);


function getVideoFile() {
clear()
Videofile().then((v) => {
setFile(v)
})
}

function getVolume(filepath: string) {
EventsOn("ffmpeg", ffmpeg)
Volume(filepath).then((v) => {
setMaxVolume(v.Max)
setMeanVolume(v.Mean)
}).finally(() => {
EventsOff("ffmpeg")
})
}

Expand All @@ -30,40 +37,58 @@ function App() {
setMeanVolume("-")
setIsLoading(false)
setNormalized("")
setStream("")
setStdout([""])
}

function ffmpeg(data: Array<string>) {
setStdout(data)
// If there is a full set of data, drop the streamed data
setStream("")
}

function handleStream(data: string) {
setStream(data)
}

function normalize(filepath: string) {
setIsLoading(true)
EventsOn("ffmpeg", ffmpeg)
EventsOn("stream", handleStream)
Normalize(filepath)
.then((result) => {
console.log(result)
setNormalized(result.Outfile)
setMaxVolume(result.Max)
setMeanVolume(result.Mean)
})
.finally(() => {
EventsOff("ffmpeg")
EventsOff("stream")
setIsLoading(false)
})
}



return (
<Box height={"100vh"} bg="brand.bg">
<Box height={"100vh"}>
<Banner />
{isLoading ? <Progress height="5px" mt="5px" mb="5px" isIndeterminate color="brand.200" /> : <Divider height="5px" mt="5px" mb="5px" />}
<LinearProgress loading={isLoading} />

<Box display="flex" justifyContent="center" width={"100vw"}>
<Stack width={"70vw"}>
<Stack direction={"row"} justify="space-between">
<Button bgColor={"brand.300"} width="140px" onClick={getVideoFile}>Select video file</Button>
<Button bgColor={"brand.300"} width="140px" onClick={clear}>Clear</Button>
<Button disabled={isLoading} bgColor={"brand.300"} width="140px" onClick={getVideoFile}>Select video file</Button>
<Button disabled={isLoading || !file} width="140px" onClick={(_) => getVolume(file)}>Analyze</Button>
<Button disabled={isLoading || !file} width="140px" onClick={(_) => normalize(file)}>Normalize Audio</Button>
<Button disabled={isLoading} bgColor={"brand.300"} width="140px" onClick={clear}>Clear</Button>
</Stack>
<Text textColor="brand.white">In: {file}</Text>
<Text textColor="brand.white">Out: {normalizedFile}</Text>
{
file
? <>
<Stack direction={"row"} justify="space-between">
<Button width="140px" onClick={(_) => getVolume(file)}>Analyze</Button>
<Button width="140px" onClick={(_) => normalize(file)}>Normalize Audio</Button>
</Stack>
<Stack direction={"row"}>
<Text textColor={'brand.white'}>max volume:</Text>
Expand All @@ -73,10 +98,11 @@ function App() {
<Text textColor={'brand.white'}>mean volume:</Text>
<Text textColor={'brand.400'}>{meanVolume}</Text>
</Stack>

<Terminal loading={isLoading} stdout={stdout} stream={stream} />
</>
: null
}

</Stack>
</Box>
</Box >
Expand All @@ -92,3 +118,49 @@ const Banner = () => {
</Box>
)
}

const LinearProgress = ({ loading }: { loading: boolean }) => {
if (loading) {
return <Progress height="5px" mt="5px" mb="5px" isIndeterminate color="brand.200" />
} else {
return <Divider height="5px" mt="5px" mb="5px" />
}
}

interface ITerminal {
loading: boolean
stdout: string[]
stream: string
}

const Terminal: React.FC<ITerminal> = ({ loading, stdout, stream }) => {

const [openTerminal, setOpenTerminal] = useState(false);

function showTerminal() {
setOpenTerminal(!openTerminal)
}

return (
<Box bg='brand.terminal' w='100%' p={4} rounded="md" >
<Box display="flex">
{stream ? <Text as="kbd" textColor="brand.white">{stream}</Text> : null}
{stdout && !loading ? <Button width="100px" onClick={showTerminal}>{openTerminal ? "Hide" : "Show"}</Button> : null}
</Box>
<Collapse in={openTerminal} >
<Stack>
{
stdout && !stream ? stdout.map(
(element: string, index: number) => {
return (
<Text fontSize="13px" key={index} as="kbd" textColor="brand.white">{element}</Text>
)
}
)
: null
}
</Stack>
</Collapse>
</Box>
)
}
19 changes: 17 additions & 2 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import { ChakraProvider } from '@chakra-ui/react'
import { ChakraProvider, StyleFunctionProps } from '@chakra-ui/react'
import { extendTheme } from '@chakra-ui/react'

const colors = {
brand: {
bg: "#303030",
white: "#fbf5f4",
terminal: "#181a1f",
200: "#f5c5b6",
300: "#ef9578",
400: "#ea653a",
},
}

const theme = extendTheme({ colors })

const theme = extendTheme(
{
colors,
styles: {
global: (props: StyleFunctionProps) => ({
body: {
color: 'default',
bg: 'brand.bg',
overflowX: 'hidden',
},
})
}
}
)

const container = document.getElementById('root')

Expand Down
67 changes: 50 additions & 17 deletions volume/main.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
package volume

import (
"bufio"
"context"
"fmt"
"os/exec"
"path/filepath"
"strings"
"syscall"

"github.com/wailsapp/wails/v2/pkg/runtime"
)

type Volume struct {
ctx context.Context
Max string
Mean string
output string
output []string
Outfile string
}

func NewVolume() Volume {
return Volume{}
func NewVolume(ctx context.Context) Volume {
return Volume{ctx: ctx}
}

// Run the volume detection filter and return the mean and max volume in dB
func (v *Volume) Volumedetect(filename string) (Volume, error) {
cmd := exec.Command("ffmpeg", "-i", filename, "-af", "volumedetect", "-vn", "-sn", "-dn", "-f", "null", "NUL")
output, err := cmd.CombinedOutput()
if err != nil {
return Volume{}, err
}
v.output = string(output)
out := v.ffmpeg("-i", filename, "-af", "volumedetect", "-vn", "-sn", "-dn", "-f", "null", "NUL")

v.output = out
v.getMean()
v.getMax()

Expand All @@ -36,11 +39,7 @@ func (v *Volume) Volumedetect(filename string) (Volume, error) {
func (v *Volume) Lufs(filename string) (Volume, error) {
ext := filepath.Ext(filename)
outfile := fmt.Sprintf("%s_normalized%s", strings.TrimSuffix(filename, ext), ext)
cmd := exec.Command("ffmpeg", "-i", filename, "-af", "loudnorm=I=-14:LRA=11:TP=-1", outfile)
_, err := cmd.CombinedOutput()
if err != nil {
return Volume{}, err
}
v.ffmpeg("-i", filename, "-af", "loudnorm=I=-14:LRA=11:TP=-1", outfile)

// Analyze after normalization
v.Volumedetect(outfile)
Expand All @@ -52,9 +51,8 @@ func (v *Volume) Lufs(filename string) (Volume, error) {
}

// Loop over ffmpeg output to grab specific lines and split on a delimiter
func getLine(find string, text string, delimiter string) string {
out := strings.Split(strings.Replace(text, "\r\n", "\n", -1), "\n")
for _, line := range out {
func getLine(find string, text []string, delimiter string) string {
for _, line := range text {
if strings.Contains(line, find) {
found_line := strings.Split(line, delimiter)
found_value := strings.Trim(found_line[1], " ")
Expand All @@ -73,3 +71,38 @@ func (v *Volume) getMean() string {
v.Mean = getLine("mean_volume", v.output, ":")
return v.Mean
}

func (v *Volume) ffmpeg(args ...string) []string {
cmd := exec.Command("ffmpeg", args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000,
}

stderr, _ := cmd.StderrPipe()
cmd.Start()

streamScanner := bufio.NewScanner(stderr)
streamScanner.Split(bufio.ScanWords)

go func() {
for streamScanner.Scan() {
m := streamScanner.Text()
runtime.EventsEmit(v.ctx, "stream", m)
}
}()

scanner := bufio.NewScanner(stderr)
scanner.Split(bufio.ScanLines)

out := []string{""}
for scanner.Scan() {
m := scanner.Text()

out = append(out, m)
}

cmd.Wait()
runtime.EventsEmit(v.ctx, "ffmpeg", out)
return out
}

0 comments on commit 1d66b7e

Please sign in to comment.