clipper/backend/media/media_set.go

139 lines
3.7 KiB
Go
Raw Normal View History

2021-09-13 18:58:28 +00:00
package media
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
2021-09-14 20:26:46 +00:00
"log"
2021-09-13 18:58:28 +00:00
"os"
2021-09-14 20:26:46 +00:00
"time"
2021-11-02 18:03:26 +00:00
"github.com/google/uuid"
2021-09-13 18:58:28 +00:00
)
const SizeOfInt16 = 2
2021-09-14 20:26:46 +00:00
type Audio struct {
2021-10-27 19:34:59 +00:00
Bytes int64 `json:"bytes"`
Channels int `json:"channels"`
// ApproxFrames is used during initial processing when a precise frame count
// cannot be determined. Prefer Frames in all other cases.
2021-11-01 05:28:40 +00:00
ApproxFrames int64 `json:"approx_frames"`
Frames int64 `json:"frames"`
SampleRate int `json:"sample_rate"`
YoutubeItag int `json:"youtube_itag"`
MimeType string `json:"mime_type"`
2021-09-14 20:26:46 +00:00
}
type Video struct {
2021-10-27 19:34:59 +00:00
Bytes int64 `json:"bytes"`
Duration time.Duration `json:"duration"`
// not sure if this are needed any more?
2021-11-01 05:28:40 +00:00
ThumbnailWidth int `json:"thumbnail_width"`
ThumbnailHeight int `json:"thumbnail_height"`
YoutubeItag int `json:"youtube_itag"`
MimeType string `json:"mime_type"`
2021-09-14 20:26:46 +00:00
}
2021-09-13 18:58:28 +00:00
// MediaSet represents the media and metadata associated with a single media
// resource (for example, a YouTube video).
type MediaSet struct {
2021-11-02 18:03:26 +00:00
Audio Audio `json:"audio"`
Video Video `json:"video"`
ID uuid.UUID `json:"id"`
YoutubeID string `json:"youtube_id"`
2021-09-13 18:58:28 +00:00
2021-11-01 05:28:40 +00:00
exists bool `json:"exists"`
2021-09-13 18:58:28 +00:00
}
2021-09-25 17:00:19 +00:00
// New builds a new MediaSet with the given ID.
2021-11-01 05:28:40 +00:00
func NewMediaSet(youtubeID string) *MediaSet {
return &MediaSet{YoutubeID: youtubeID}
2021-09-25 17:00:19 +00:00
}
2021-09-14 20:26:46 +00:00
// TODO: pass io.Readers/Writers instead of strings.
2021-11-01 05:28:40 +00:00
func (m *MediaSet) RawAudioPath() string { return fmt.Sprintf("cache/%s.raw", m.YoutubeID) }
func (m *MediaSet) EncodedAudioPath() string { return fmt.Sprintf("cache/%s.m4a", m.YoutubeID) }
func (m *MediaSet) VideoPath() string { return fmt.Sprintf("cache/%s.mp4", m.YoutubeID) }
func (m *MediaSet) ThumbnailPath() string { return fmt.Sprintf("cache/%s.jpg", m.YoutubeID) }
func (m *MediaSet) MetadataPath() string { return fmt.Sprintf("cache/%s.json", m.YoutubeID) }
2021-09-13 18:58:28 +00:00
func (m *MediaSet) Exists() bool {
2021-11-01 05:28:40 +00:00
if m.YoutubeID == "" {
2021-09-14 20:26:46 +00:00
return false
}
2021-09-13 18:58:28 +00:00
if m.exists {
return true
}
if _, err := os.Stat(m.MetadataPath()); err == nil {
m.exists = true
return true
}
return false
}
func (m *MediaSet) Load() error {
2021-11-01 05:28:40 +00:00
if m.YoutubeID == "" {
2021-09-14 20:26:46 +00:00
return errors.New("error opening mediaset with blank ID")
}
2021-09-13 18:58:28 +00:00
metadataFile, err := os.Open(m.MetadataPath())
if err != nil {
return fmt.Errorf("error opening metadata file: %v", err)
}
defer func() { _ = metadataFile.Close() }()
if err := json.NewDecoder(metadataFile).Decode(m); err != nil {
return fmt.Errorf("error decoding metadata: %v", err)
}
return nil
}
func (m *MediaSet) Peaks(start, end int64, numBins int) ([][]int16, error) {
if !m.Exists() {
return nil, errors.New("cannot compute peaks for non-existent MediaSet")
}
var err error
fptr, err := os.Open(m.RawAudioPath())
if err != nil {
return nil, fmt.Errorf("audio open error: %v", err)
}
defer fptr.Close()
2021-09-14 20:26:46 +00:00
startByte := start * int64(m.Audio.Channels) * SizeOfInt16
2021-09-13 18:58:28 +00:00
if _, err = fptr.Seek(startByte, io.SeekStart); err != nil {
return nil, fmt.Errorf("audio seek error: %v", err)
}
numFrames := end - start
framesPerBin := numFrames / int64(numBins)
2021-09-14 20:26:46 +00:00
peaks := make([][]int16, m.Audio.Channels)
for i := 0; i < m.Audio.Channels; i++ {
2021-09-13 18:58:28 +00:00
peaks[i] = make([]int16, numBins)
}
2021-09-14 20:26:46 +00:00
samples := make([]int16, framesPerBin*int64(m.Audio.Channels))
2021-09-13 18:58:28 +00:00
for binNum := 0; binNum < numBins; binNum++ {
if err := binary.Read(fptr, binary.LittleEndian, samples); err != nil {
return nil, fmt.Errorf("error reading samples: %v", err)
}
for i, samp := range samples {
if samp < 0 {
samp = -samp
}
2021-09-14 20:26:46 +00:00
chanIndex := i % m.Audio.Channels
2021-09-13 18:58:28 +00:00
if samp > peaks[chanIndex][binNum] {
peaks[chanIndex][binNum] = samp
}
}
}
2021-09-14 20:26:46 +00:00
log.Println("finished generating peaks")
2021-09-13 18:58:28 +00:00
return peaks, nil
}