Add audio streaming support with WebRTC and Opus (#102)

* Update README.md with additional info about Windows builds

* Add initial Opus stream audio support
This commit is contained in:
sergystepanov 2019-10-08 22:02:09 +03:00 committed by giongto35
parent 937bbb2857
commit 4e06d6a27e
8 changed files with 39 additions and 38 deletions

2
go.mod vendored
View file

@ -9,8 +9,6 @@ require (
github.com/gofrs/uuid v3.2.0+incompatible
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/gorilla/websocket v1.4.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pion/webrtc/v2 v2.1.2
github.com/prometheus/client_golang v1.1.0
github.com/spf13/pflag v1.0.3

View file

@ -10,6 +10,11 @@ const DefaultSTUNTURN = `[{"urls":"stun:stun-turn.webgame2d.com:3478"},{"urls":"
const CODEC_VP8 = "VP8"
const CODEC_H264 = "H264"
const AUDIO_RATE = 48000
const AUDIO_CHANNELS = 2
const AUDIO_MS = 10
const AUDIO_FRAME = AUDIO_RATE * AUDIO_MS / 1000 * AUDIO_CHANNELS
var Port = flag.String("port", "8000", "Port of the game")
var FrontendSTUNTURN = flag.String("stunturn", DefaultSTUNTURN, "Frontend STUN TURN servers")
var Mode = flag.String("mode", "dev", "Environment")

View file

@ -48,7 +48,7 @@ import "C"
// naEmulator implements CloudEmulator
type naEmulator struct {
imageChannel chan<- *image.RGBA
audioChannel chan<- float32
audioChannel chan<- int16
inputChannel <-chan int
meta config.EmulatorMeta
@ -65,7 +65,7 @@ var NAEmulator *naEmulator
var outputImg *image.RGBA
// NAEmulator implements CloudEmulator interface based on NanoArch(golang RetroArch)
func NewNAEmulator(etype string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- float32, inputChannel <-chan int) *naEmulator {
func NewNAEmulator(etype string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- int16, inputChannel <-chan int) *naEmulator {
meta := config.EmulatorConfig[etype]
ewidth = meta.Width
eheight = meta.Height
@ -84,7 +84,7 @@ func NewNAEmulator(etype string, roomID string, imageChannel chan<- *image.RGBA,
}
// Init initialize new RetroArch cloud emulator
func Init(etype string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- float32, inputChannel <-chan int) {
func Init(etype string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- int16, inputChannel <-chan int) {
NAEmulator = NewNAEmulator(etype, roomID, imageChannel, audioChannel, inputChannel)
go NAEmulator.listenInput()
}

View file

@ -234,18 +234,17 @@ func min(a, b C.size_t) C.size_t {
}
func audioWrite2(buf unsafe.Pointer, frames C.size_t) C.size_t {
numFrames := int(frames) * 2
pcm := (*[1 << 30]int16)(unsafe.Pointer(buf))[:numFrames:numFrames]
// !to make it mono/stereo independent
samples := int(frames) * 2
pcm := (*[1 << 30]int16)(buf)[:samples:samples]
for i := 0; i < numFrames; i += 1 {
s := float32(pcm[i])
select {
case NAEmulator.audioChannel <- s:
default:
}
// !to rewrite this stuff
// (channels are not that fast to put bytes one by one)
for i := 0; i < samples; i += 1 {
NAEmulator.audioChannel <- pcm[i]
}
return 2 * frames
return frames
}
//export coreAudioSample

View file

@ -291,7 +291,10 @@ func (w *WebRTC) startStreaming(vp8Track *webrtc.Track, opusTrack *webrtc.Track)
if !w.isConnected {
return
}
err := opusTrack.WriteSample(media.Sample{Data: data, Samples: uint32(len(data))})
err := opusTrack.WriteSample(media.Sample{
Data: data,
Samples: uint32(config.AUDIO_FRAME / config.AUDIO_CHANNELS),
})
if err != nil {
log.Println("Warn: Err write sample: ", err)
}

View file

@ -11,10 +11,10 @@ import (
"gopkg.in/hraban/opus.v2"
)
func resample(pcm []float32, targetSize int, srcSampleRate int, dstSampleRate int) []float32 {
newPCML := make([]float32, targetSize/2)
newPCMR := make([]float32, targetSize/2)
newPCM := make([]float32, targetSize)
func resample(pcm []int16, targetSize int, srcSampleRate int, dstSampleRate int) []int16 {
newPCML := make([]int16, targetSize/2)
newPCMR := make([]int16, targetSize/2)
newPCM := make([]int16, targetSize)
for i := 0; i+1 < len(pcm); i += 2 {
newPCML[(i/2)*dstSampleRate/srcSampleRate] = pcm[i]
newPCMR[(i/2)*dstSampleRate/srcSampleRate] = pcm[i+1]
@ -39,29 +39,25 @@ func resample(pcm []float32, targetSize int, srcSampleRate int, dstSampleRate in
func (r *Room) startAudio(sampleRate int) {
log.Println("Enter fan audio")
// srcSampleRate := 32768
srcSampleRate := sampleRate
dstSampleRate := 48000
enc, err := opus.NewEncoder(dstSampleRate, 2, opus.AppVoIP)
enc, err := opus.NewEncoder(config.AUDIO_RATE, 2, opus.AppAudio)
if err != nil {
log.Println("[!] Cannot create audio encoder", err)
}
enc.SetMaxBandwidth(opus.Fullband)
enc.SetBitrateToAuto()
enc.SetComplexity(10)
dstBufferSize := 240
srcBufferSize := dstBufferSize * srcSampleRate / dstSampleRate
dstBufferSize := config.AUDIO_FRAME
srcBufferSize := dstBufferSize * srcSampleRate / config.AUDIO_RATE
fmt.Println("src BufferSize", srcBufferSize)
pcm := make([]float32, srcBufferSize) // 640 * 1000 / 16000 == 40 ms
pcm := make([]int16, srcBufferSize) // 640 * 1000 / 16000 == 40 ms
idx := 0
if err != nil {
log.Println("[!] Cannot create audio encoder", err)
return
}
// fanout Audio
fmt.Println("listening audiochanel", r.IsRunning)
fmt.Println("listening audio channel", r.IsRunning)
for sample := range r.audioChannel {
if !r.IsRunning {
log.Println("Room ", r.ID, " audio channel closed")
@ -72,9 +68,9 @@ func (r *Room) startAudio(sampleRate int) {
pcm[idx] = sample
idx++
if idx == len(pcm) {
data := make([]byte, dstBufferSize)
dstpcm := resample(pcm, dstBufferSize, srcSampleRate, dstSampleRate)
n, err := enc.EncodeFloat32(dstpcm, data)
data := make([]byte, 1024*2)
dstpcm := resample(pcm, dstBufferSize, srcSampleRate, config.AUDIO_RATE)
n, err := enc.Encode(dstpcm, data)
if err != nil {
log.Println("[!] Failed to decode", err)

View file

@ -28,7 +28,7 @@ type Room struct {
// imageChannel is image stream received from director
imageChannel <-chan *image.RGBA
// audioChannel is audio stream received from director
audioChannel <-chan float32
audioChannel <-chan int16
// inputChannel is input stream from websocket send to room
inputChannel chan<- int
// State of room
@ -63,7 +63,7 @@ func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStor
log.Println("Init new room: ", roomID, gameName, gameInfo)
imageChannel := make(chan *image.RGBA, 30)
audioChannel := make(chan float32, 30)
audioChannel := make(chan int16, 30)
inputChannel := make(chan int, 100)
room := &Room{
@ -115,7 +115,7 @@ func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStor
}
// create director
func getEmulator(emuName string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- float32, inputChannel <-chan int) emulator.CloudEmulator {
func getEmulator(emuName string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- int16, inputChannel <-chan int) emulator.CloudEmulator {
nanoarch.Init(emuName, roomID, imageChannel, audioChannel, inputChannel)
return nanoarch.NAEmulator

2
web/game.html vendored
View file

@ -33,7 +33,7 @@
There is still audio because current audio flow is not from media but it is manually encoded (technical webRTC challenge). Later, when we can integrate audio to media, we can face the issue with mute again .
https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
-->
<video id="game-screen" muted playinfullscreen="false" playsinline poster="/static/img/screen_loading.gif"></video>
<video id="game-screen" playinfullscreen="false" playsinline onloadstart="this.volume=0.5" poster="/static/img/screen_loading.gif"></video>
<!--<video id="game-screen" autoplay=true poster="/static/img/screen_loading.gif"></video>-->
<div id="menu-screen">