Compare commits
3 commits
0c4167a1f5
...
637311168c
Author | SHA1 | Date | |
---|---|---|---|
DataHoarder | 637311168c | ||
DataHoarder | 060d4e23ce | ||
DataHoarder | 120df4a734 |
|
@ -1,3 +1,4 @@
|
|||
testdata/*.ivf
|
||||
testdata/*.y4m
|
||||
testdata/*.y4m.xz
|
||||
testdata/testoutput
|
29
README.md
29
README.md
|
@ -1,10 +1,35 @@
|
|||
|
||||
# Supported
|
||||
* y4m pipes
|
||||
* 4:4:4, 4:2:0, and probably 4:2:2 and 4:0:0.
|
||||
* 8, 10, 12 bit depth. Probably 14 and 16 as well.
|
||||
* Frame tested support for 4:4:4, 4:2:2, 4:2:0. Probably 4:0:0 as well, untested.
|
||||
* Frame tested support for 8, 10, 12 bit depth. Probably 14 and 16 as well, untested.
|
||||
* VMAF tools
|
||||
* IVF reader
|
||||
* Frameserver
|
||||
* TODO: make list per encoder and decoder.
|
||||
|
||||
## Bitdepth / subsampling Frame support matrix
|
||||
|
||||
| Depth/Sampling | 4:0:0 | 4:2:0 | 4:2:2 | 4:4:4 |
|
||||
|:--------------:|:-----:|:-----:|:-----:|:-----:|
|
||||
| 8-bit | ⚠️ | ✅ | ✅ | ✅ |
|
||||
| 10-bit | ⚠️ | ✅ | ⚠️ | ⚠️ |
|
||||
| 12-bit | ⚠️ | ⚠️ | ⚠️ | ✅ |
|
||||
| 14-bit | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
|
||||
| 16-bit | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
|
||||
|
||||
Legend: ✅ = supported, tested; ️⚠️ = supported, untested; ❌ = unsupported
|
||||
|
||||
## Formats supported
|
||||
|
||||
| Format | Decoder | Encoder | Notes |
|
||||
|:-------------:|:-------:|:-------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **YUV4MPEG2** | ✅ | ❌ | Supports most bitdepth and chroma subsampling. Library limited, not format limited.</br>Decoding via [S.O.N.G/Ignite](https://git.gammaspectra.live/S.O.N.G/Ignite) |
|
||||
| **H.264** | ❌ | ✅ | Supports 8-bit and 10-bit; 4:0:0, 4:2:0, 4:2:2, 4:4:4 chroma subsampling.</br>Encoding via [x264](https://code.videolan.org/videolan/x264) into .h264 bitstream. |
|
||||
| **H.265** | ❌ | ❌ | |
|
||||
| **VP9** | ❌ | ❌ | |
|
||||
| **AV1** | ✅ | ✅ | Supports 8-bit, 10-bit and 12-bit; 4:0:0, 4:2:0, 4:2:2, 4:4:4 chroma subsampling.</br>Decoding via [dav1d](https://code.videolan.org/videolan/dav1d) from .ivf bitstream</br>Encoding via [libaom-av1](https://aomedia.googlesource.com/aom) into .ivf bitstream. |
|
||||
|
||||
# TODO
|
||||
* No SAR/PAR handling.
|
||||
* No color primary / transfer / matrix coefficients handling.
|
||||
|
|
|
@ -81,6 +81,14 @@ func (c Space) check() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func MustColorFormatFromString(colorFormat string) Space {
|
||||
s, err := NewColorFormatFromString(colorFormat)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func NewColorFormatFromString(colorFormat string) (Space, error) {
|
||||
colorFormat = strings.ToLower(colorFormat)
|
||||
if colorFormat == "420paldv" {
|
||||
|
|
16
decoder/libdav1d/libdav1d.c
Normal file
16
decoder/libdav1d/libdav1d.c
Normal file
|
@ -0,0 +1,16 @@
|
|||
#include "libdav1d.h"
|
||||
#include <math.h>
|
||||
|
||||
#define DAV1D_VERSION_AT_LEAST(x,y) \
|
||||
(DAV1D_API_VERSION_MAJOR > (x) || DAV1D_API_VERSION_MAJOR == (x) && DAV1D_API_VERSION_MINOR >= (y))
|
||||
|
||||
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
|
||||
|
||||
void set_threading(Dav1dSettings* s, int threads) {
|
||||
#if DAV1D_VERSION_AT_LEAST(6,0)
|
||||
|
||||
#else
|
||||
s->n_tile_threads = MIN(floor(sqrt(threads)), DAV1D_MAX_TILE_THREADS);
|
||||
s->n_frame_threads = MIN(ceil(threads / s->n_tile_threads), DAV1D_MAX_FRAME_THREADS);
|
||||
#endif
|
||||
}
|
|
@ -4,7 +4,8 @@ package libdav1d
|
|||
|
||||
/*
|
||||
#cgo pkg-config: dav1d
|
||||
#include <dav1d/dav1d.h>
|
||||
#cgo LDFLAGS: -lm
|
||||
#include "libdav1d.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
|
@ -57,6 +58,7 @@ func NewDecoder(r io.Reader, settings map[string]any) (d *Decoder, err error) {
|
|||
}
|
||||
|
||||
C.dav1d_default_settings(&d.settings)
|
||||
C.set_threading(&d.settings, C.int(runtime.NumCPU()))
|
||||
if ret := C.dav1d_open(&d.ctx, &d.settings); ret != 0 {
|
||||
return nil, fmt.Errorf("error %d", ret)
|
||||
}
|
||||
|
@ -201,6 +203,15 @@ func (d *Decoder) pictureToFrame() (frame.Frame, error) {
|
|||
properties.FullColorRange = true
|
||||
}
|
||||
|
||||
properties.ColorSpace.ChromaSamplePosition = color.ChromaSamplePositionUnspecified
|
||||
if d.picture.seq_hdr.chr == C.DAV1D_CHR_UNKNOWN {
|
||||
properties.ColorSpace.ChromaSamplePosition = color.ChromaSamplePositionCenter
|
||||
} else if d.picture.seq_hdr.chr == C.DAV1D_CHR_VERTICAL {
|
||||
properties.ColorSpace.ChromaSamplePosition = color.ChromaSamplePositionLeft
|
||||
} else if d.picture.seq_hdr.chr == C.DAV1D_CHR_COLOCATED {
|
||||
properties.ColorSpace.ChromaSamplePosition = color.ChromaSamplePositionTopLeft
|
||||
}
|
||||
|
||||
defer C.dav1d_picture_unref(&d.picture)
|
||||
|
||||
if bitDepth > 8 { //16-bit
|
||||
|
|
3
decoder/libdav1d/libdav1d.h
Normal file
3
decoder/libdav1d/libdav1d.h
Normal file
|
@ -0,0 +1,3 @@
|
|||
#include <dav1d/dav1d.h>
|
||||
|
||||
void set_threading(Dav1dSettings* s, int threads);
|
|
@ -3,7 +3,10 @@
|
|||
package libdav1d
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/testdata"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
@ -12,28 +15,77 @@ func TestVersion(t *testing.T) {
|
|||
t.Logf("dav1d version: %s", Version())
|
||||
}
|
||||
|
||||
func TestDecodeYUV420_8bit(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
f, err := os.Open(testdata.AV1_Sintel_Trailer_720p24_YUV420_8bit_Low)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
func testDecode(sample testdata.TestSample, t *testing.T) {
|
||||
var reader io.Reader
|
||||
var err error
|
||||
|
||||
if decoder, err := NewDecoder(f, nil); err != nil {
|
||||
if _, err = os.Stat(sample.Path); err != nil {
|
||||
if sample.SkipNotFound || sample.Url == "" {
|
||||
t.Skip("skipping without sample")
|
||||
}
|
||||
response, err := http.DefaultClient.Get(sample.Url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
reader = response.Body
|
||||
} else {
|
||||
f, err := os.Open(sample.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
reader = f
|
||||
}
|
||||
|
||||
if decoder, err := NewDecoder(reader, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
defer decoder.Close()
|
||||
decoded := 0
|
||||
for range decoder.DecodeStream().Channel() {
|
||||
|
||||
var frameProperties frame.Properties
|
||||
for decodedFrame := range decoder.DecodeStream().Channel() {
|
||||
if decoded == 0 {
|
||||
frameProperties = decodedFrame.Properties()
|
||||
}
|
||||
//ingest
|
||||
decoded++
|
||||
if decoded%50 == 0 {
|
||||
t.Logf("%d/%d", decoded, sample.Frames)
|
||||
}
|
||||
}
|
||||
|
||||
if decoded != 1253 {
|
||||
t.Fatalf("expected %d frames, got %d", 1253, decoded)
|
||||
if decoded != sample.Frames {
|
||||
t.Fatalf("expected %d frames, got %d", sample.Frames, decoded)
|
||||
}
|
||||
|
||||
if frameProperties.Width != sample.Width {
|
||||
t.Fatalf("expected %d width, got %d", sample.Width, frameProperties.Width)
|
||||
}
|
||||
|
||||
if frameProperties.Height != sample.Height {
|
||||
t.Fatalf("expected %d height, got %d", sample.Height, frameProperties.Height)
|
||||
}
|
||||
|
||||
if frameProperties.ColorSpace.String() != sample.ColorSpace.String() {
|
||||
t.Fatalf("expected %s color space, got %s", sample.ColorSpace.String(), frameProperties.ColorSpace.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecode_YUV420_8bit(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
testDecode(testdata.AV1_Sintel_Trailer_720p24_YUV420_8bit_Low, t)
|
||||
}
|
||||
|
||||
func TestDecode_YUV444_12bit(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
testDecode(testdata.AV1_Netflix_Sol_Levante_2160p24_YUV444_12bit_Lossy, t)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/color"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
||||
"github.com/ulikunitz/xz"
|
||||
"io"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
@ -43,6 +44,14 @@ const (
|
|||
const fileMagic = "YUV4MPEG2 "
|
||||
const frameMagic = "FRAME"
|
||||
|
||||
func NewXZCompressedDecoder(reader io.Reader, settings map[string]any) (*Decoder, error) {
|
||||
r, err := xz.NewReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewDecoder(r, settings)
|
||||
}
|
||||
|
||||
func NewDecoder(reader io.Reader, settings map[string]any) (*Decoder, error) {
|
||||
s := &Decoder{
|
||||
r: reader,
|
||||
|
|
|
@ -1,30 +1,100 @@
|
|||
package y4m
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/testdata"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeYUV420_8bit(t *testing.T) {
|
||||
f, err := os.Open(testdata.Y4M_Sintel_Trailer_720p24_YUV420_8bit)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
func testDecode(sample testdata.TestSample, t *testing.T) {
|
||||
var reader io.Reader
|
||||
var err error
|
||||
|
||||
if y4m, err := NewDecoder(f, nil); err != nil {
|
||||
if _, err = os.Stat(sample.Path); err != nil {
|
||||
if sample.SkipNotFound || sample.Url == "" {
|
||||
t.Skip("skipping without sample")
|
||||
}
|
||||
response, err := http.DefaultClient.Get(sample.Url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
reader = response.Body
|
||||
} else {
|
||||
f, err := os.Open(sample.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
reader = f
|
||||
}
|
||||
|
||||
var y4m *Decoder
|
||||
|
||||
switch sample.Type {
|
||||
case "y4m":
|
||||
y4m, err = NewDecoder(reader, nil)
|
||||
case "y4m.xz":
|
||||
y4m, err = NewXZCompressedDecoder(reader, nil)
|
||||
default:
|
||||
t.Fatal("unsupported sample type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
defer y4m.Close()
|
||||
decoded := 0
|
||||
for range y4m.DecodeStream().Channel() {
|
||||
|
||||
var frameProperties frame.Properties
|
||||
for decodedFrame := range y4m.DecodeStream().Channel() {
|
||||
if decoded == 0 {
|
||||
frameProperties = decodedFrame.Properties()
|
||||
}
|
||||
//ingest
|
||||
decoded++
|
||||
if decoded%50 == 0 {
|
||||
t.Logf("%d/%d", decoded, sample.Frames)
|
||||
}
|
||||
}
|
||||
|
||||
if decoded != 1253 {
|
||||
t.Fatalf("expected %d frames, got %d", 1253, decoded)
|
||||
if decoded != sample.Frames {
|
||||
t.Fatalf("expected %d frames, got %d", sample.Frames, decoded)
|
||||
}
|
||||
|
||||
if frameProperties.Width != sample.Width {
|
||||
t.Fatalf("expected %d width, got %d", sample.Width, frameProperties.Width)
|
||||
}
|
||||
|
||||
if frameProperties.Height != sample.Height {
|
||||
t.Fatalf("expected %d height, got %d", sample.Height, frameProperties.Height)
|
||||
}
|
||||
|
||||
if frameProperties.ColorSpace.String() != sample.ColorSpace.String() {
|
||||
t.Fatalf("expected %s color space, got %s", sample.ColorSpace.String(), frameProperties.ColorSpace.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecode_YUV420_720p24_8bit(t *testing.T) {
|
||||
testDecode(testdata.Y4M_Sintel_Trailer_720p24_YUV420_8bit, t)
|
||||
}
|
||||
|
||||
func TestDecode_YUV444_720p50_8bit(t *testing.T) {
|
||||
testDecode(testdata.Y4M_Ducks_Take_Off_720p50_YUV444_8bit, t)
|
||||
}
|
||||
|
||||
func TestDecode_YUV422_720p50_8bit(t *testing.T) {
|
||||
testDecode(testdata.Y4M_Ducks_Take_Off_720p50_YUV422_8bit, t)
|
||||
}
|
||||
|
||||
func TestDecode_YUV420_2160p60_10bit(t *testing.T) {
|
||||
testDecode(testdata.Y4M_Netflix_FoodMarket_2160p60_YUV420_10bit, t)
|
||||
}
|
||||
|
||||
func TestDecode_YUV420_360p24_8bit_xz(t *testing.T) {
|
||||
testDecode(testdata.Y4M_Big_Buck_Bunny_360p24_YUV420_8bit, t)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ package libaom
|
|||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/decoder/y4m"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/testdata"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
@ -15,20 +17,45 @@ func TestVersion(t *testing.T) {
|
|||
t.Logf("libaom version: %s", Version())
|
||||
}
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
f, err := os.Open(testdata.Y4M_Sintel_Trailer_720p24_YUV420_8bit)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
func testEncode(sample testdata.TestSample, t *testing.T) {
|
||||
var reader io.Reader
|
||||
var err error
|
||||
|
||||
if pipe, err := y4m.NewDecoder(f, nil); err != nil {
|
||||
if _, err = os.Stat(sample.Path); err != nil {
|
||||
if sample.SkipNotFound || sample.Url == "" {
|
||||
t.Skip("skipping without sample")
|
||||
}
|
||||
response, err := http.DefaultClient.Get(sample.Url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
reader = response.Body
|
||||
} else {
|
||||
f, err := os.Open(sample.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
reader = f
|
||||
}
|
||||
|
||||
var pipe *y4m.Decoder
|
||||
|
||||
switch sample.Type {
|
||||
case "y4m":
|
||||
pipe, err = y4m.NewDecoder(reader, nil)
|
||||
case "y4m.xz":
|
||||
pipe, err = y4m.NewXZCompressedDecoder(reader, nil)
|
||||
default:
|
||||
t.Fatal("unsupported sample type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
defer pipe.Close()
|
||||
|
||||
target, err := os.CreateTemp("", "encode_test_*.ivf")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -51,7 +78,7 @@ func TestEncode(t *testing.T) {
|
|||
settings["frame-parallel"] = 1
|
||||
settings["lag-in-frames"] = 2
|
||||
settings["tile-columns"] = 1
|
||||
settings["tile-rows"] = 1
|
||||
settings["tile-rows"] = 4
|
||||
|
||||
if encoder, err := NewEncoder(target, stream.Properties(), settings); err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -65,7 +92,7 @@ func TestEncode(t *testing.T) {
|
|||
defer wg.Done()
|
||||
for range s[0].Channel() {
|
||||
if encoded%10 == 0 {
|
||||
t.Logf("frame %d/1253", encoded)
|
||||
t.Logf("frame %d/%d", encoded, sample.Frames)
|
||||
}
|
||||
encoded++
|
||||
}
|
||||
|
@ -77,9 +104,17 @@ func TestEncode(t *testing.T) {
|
|||
|
||||
wg.Wait()
|
||||
|
||||
if encoded != 1253 {
|
||||
t.Fatalf("expected %d frames, got %d", 1253, encoded)
|
||||
if encoded != sample.Frames {
|
||||
t.Fatalf("expected %d frames, got %d", sample.Frames, encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncode_YUV420_8bit(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
|
||||
testEncode(testdata.Y4M_Sintel_Trailer_720p24_YUV420_8bit, t)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,21 @@
|
|||
#include "libx264.h"
|
||||
|
||||
#include "_cgo_export.h"
|
||||
|
||||
const char* Version() {
|
||||
return X264_VERSION;
|
||||
}
|
||||
|
||||
void SetLogCallback( x264_param_t *p ) {
|
||||
p->pf_log = LogCallback;
|
||||
}
|
||||
|
||||
void LogCallback( void *p, int i_level, const char *psz_fmt, va_list arg ) {
|
||||
char *message = malloc(4096+1);
|
||||
int n = vsnprintf(message, 4096, psz_fmt, arg );
|
||||
if (n > 0 && n < 4096) {
|
||||
//success
|
||||
logCallback(i_level, message, n, p);
|
||||
}
|
||||
free(message);
|
||||
}
|
|
@ -11,8 +11,11 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/utilities"
|
||||
"io"
|
||||
"log"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
@ -34,15 +37,23 @@ func Version() string {
|
|||
return x264Version
|
||||
}
|
||||
|
||||
const (
|
||||
LogLevelNone int = -1
|
||||
LogLevelError int = 0
|
||||
LogLevelWarning int = 1
|
||||
LogLevelInfo int = 2
|
||||
LogLevelDebug int = 3
|
||||
)
|
||||
|
||||
func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[string]any) (*Encoder, error) {
|
||||
e := &Encoder{
|
||||
w: w,
|
||||
}
|
||||
|
||||
preset := C.CString(getSettingString(settings, "preset", "medium"))
|
||||
preset := C.CString(utilities.GetSettingString(settings, "preset", "medium"))
|
||||
defer C.free(unsafe.Pointer(preset))
|
||||
var tune *C.char
|
||||
if strVal := getSettingString(settings, "tune", ""); strVal != "" {
|
||||
if strVal := utilities.GetSettingString(settings, "tune", ""); strVal != "" {
|
||||
tune = C.CString(strVal)
|
||||
defer C.free(unsafe.Pointer(tune))
|
||||
}
|
||||
|
@ -78,7 +89,7 @@ func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[str
|
|||
}
|
||||
}
|
||||
|
||||
profile := C.CString(getSettingString(settings, "profile", defaultProfile))
|
||||
profile := C.CString(utilities.GetSettingString(settings, "profile", defaultProfile))
|
||||
defer C.free(unsafe.Pointer(profile))
|
||||
|
||||
e.params.i_bitdepth = C.int(properties.ColorSpace.BitDepth)
|
||||
|
@ -157,6 +168,9 @@ func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[str
|
|||
return nil, errors.New("error allocating input picture")
|
||||
}
|
||||
|
||||
e.params.p_log_private = unsafe.Pointer(cgo.NewHandle(e))
|
||||
C.SetLogCallback(&e.params)
|
||||
|
||||
if e.h = C.x264_encoder_open(&e.params); e.h == nil {
|
||||
return nil, errors.New("error opening encoder")
|
||||
}
|
||||
|
@ -174,10 +188,6 @@ func (e *Encoder) EncodeStream(stream *frame.Stream) error {
|
|||
}
|
||||
|
||||
func (e *Encoder) Encode(f frame.Frame) error {
|
||||
var nal *C.x264_nal_t
|
||||
var iNal C.int
|
||||
var frameSize C.int
|
||||
|
||||
var luma, cb, cr unsafe.Pointer
|
||||
|
||||
switch typedFrame := f.(type) {
|
||||
|
@ -213,11 +223,19 @@ func (e *Encoder) Encode(f frame.Frame) error {
|
|||
|
||||
e.pictureIn.i_pts = C.int64_t(f.PTS())
|
||||
|
||||
if frameSize = C.x264_encoder_encode(e.h, &nal, &iNal, e.pictureIn, &e.pictureOut); frameSize < 0 {
|
||||
return e.encoderEncode(e.pictureIn)
|
||||
}
|
||||
|
||||
func (e *Encoder) encoderEncode(picture *C.x264_picture_t) error {
|
||||
var nal *C.x264_nal_t
|
||||
var iNal C.int
|
||||
var frameSize C.int
|
||||
|
||||
if frameSize = C.x264_encoder_encode(e.h, &nal, &iNal, picture, &e.pictureOut); frameSize < 0 {
|
||||
return errors.New("error encoding frame")
|
||||
}
|
||||
|
||||
if frameSize == 0 {
|
||||
if frameSize == 0 || iNal == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -225,6 +243,7 @@ func (e *Encoder) Encode(f frame.Frame) error {
|
|||
//return errors.New("more than one NAL present")
|
||||
}
|
||||
|
||||
// All NAL payloads are sequential to each other in memory
|
||||
buf := unsafe.Slice((*byte)(nal.p_payload), int(frameSize))
|
||||
if _, err := e.w.Write(buf); err != nil {
|
||||
return err
|
||||
|
@ -234,33 +253,32 @@ func (e *Encoder) Encode(f frame.Frame) error {
|
|||
}
|
||||
|
||||
func (e *Encoder) Flush() error {
|
||||
var nal *C.x264_nal_t
|
||||
var iNal C.int
|
||||
var frameSize C.int
|
||||
|
||||
for C.x264_encoder_delayed_frames(e.h) > 0 {
|
||||
if frameSize = C.x264_encoder_encode(e.h, &nal, &iNal, nil, &e.pictureOut); frameSize < 0 {
|
||||
return errors.New("error encoding frame")
|
||||
}
|
||||
|
||||
if frameSize == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if iNal != 1 {
|
||||
//return errors.New("more than one NAL present")
|
||||
}
|
||||
|
||||
buf := unsafe.Slice((*byte)(nal.p_payload), int(frameSize))
|
||||
if _, err := e.w.Write(buf); err != nil {
|
||||
if err := e.encoderEncode(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Encoder) Log(logLevel int, message string) {
|
||||
var prefix string
|
||||
switch logLevel {
|
||||
case C.X264_LOG_ERROR:
|
||||
prefix = "error"
|
||||
case C.X264_LOG_WARNING:
|
||||
prefix = "warning"
|
||||
case C.X264_LOG_INFO:
|
||||
prefix = "info"
|
||||
case C.X264_LOG_DEBUG:
|
||||
prefix = "debug"
|
||||
default:
|
||||
prefix = "unknown"
|
||||
}
|
||||
log.Printf("x264 [%s]: %s", prefix, message)
|
||||
}
|
||||
|
||||
func (e *Encoder) Version() string {
|
||||
return Version()
|
||||
}
|
||||
|
@ -276,20 +294,14 @@ func (e *Encoder) Close() {
|
|||
C.free(unsafe.Pointer(e.pictureIn))
|
||||
e.pictureIn = nil
|
||||
}
|
||||
if e.params.p_log_private != nil {
|
||||
(cgo.Handle)(e.params.p_log_private).Delete()
|
||||
e.params.p_log_private = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getSettingString(m map[string]any, name string, fallback string) string {
|
||||
if v, ok := m[name]; ok {
|
||||
if val, ok := v.(string); ok {
|
||||
return val
|
||||
}
|
||||
if val, ok := v.(int); ok {
|
||||
return strconv.Itoa(val)
|
||||
}
|
||||
if val, ok := v.(int64); ok {
|
||||
return strconv.Itoa(int(val))
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
//export logCallback
|
||||
func logCallback(logLevel C.int, message *C.char, size C.size_t, data unsafe.Pointer) {
|
||||
(cgo.Handle)(data).Value().(*Encoder).Log(int(logLevel), C.GoStringN(message, C.int(size)))
|
||||
}
|
||||
|
|
|
@ -3,4 +3,8 @@
|
|||
#include <stdlib.h>
|
||||
#include <x264.h>
|
||||
|
||||
const char* Version();
|
||||
const char* Version();
|
||||
|
||||
void LogCallback( void *p, int i_level, const char *psz_fmt, va_list arg);
|
||||
|
||||
void SetLogCallback( x264_param_t *p );
|
|
@ -5,6 +5,8 @@ package libx264
|
|||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/decoder/y4m"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/testdata"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
@ -15,20 +17,45 @@ func TestVersion(t *testing.T) {
|
|||
t.Logf("libx264 version: %s", Version())
|
||||
}
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping encode test in short mode")
|
||||
}
|
||||
f, err := os.Open(testdata.Y4M_Sintel_Trailer_720p24_YUV420_8bit)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
func testEncode(sample testdata.TestSample, t *testing.T) {
|
||||
var reader io.Reader
|
||||
var err error
|
||||
|
||||
if pipe, err := y4m.NewDecoder(f, nil); err != nil {
|
||||
if _, err = os.Stat(sample.Path); err != nil {
|
||||
if sample.SkipNotFound || sample.Url == "" {
|
||||
t.Skip("skipping without sample")
|
||||
}
|
||||
response, err := http.DefaultClient.Get(sample.Url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
reader = response.Body
|
||||
} else {
|
||||
f, err := os.Open(sample.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
reader = f
|
||||
}
|
||||
|
||||
var pipe *y4m.Decoder
|
||||
|
||||
switch sample.Type {
|
||||
case "y4m":
|
||||
pipe, err = y4m.NewDecoder(reader, nil)
|
||||
case "y4m.xz":
|
||||
pipe, err = y4m.NewXZCompressedDecoder(reader, nil)
|
||||
default:
|
||||
t.Fatal("unsupported sample type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
defer pipe.Close()
|
||||
|
||||
target, err := os.CreateTemp("", "encode_test_*.h264")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -46,7 +73,17 @@ func TestEncode(t *testing.T) {
|
|||
settings := make(map[string]any)
|
||||
settings["threads"] = runtime.NumCPU()
|
||||
settings["preset"] = "fast"
|
||||
settings["profile"] = "high"
|
||||
settings["log"] = LogLevelInfo
|
||||
|
||||
if sample.ColorSpace.ChromaSampling.J == 4 && sample.ColorSpace.ChromaSampling.A == 4 && sample.ColorSpace.ChromaSampling.B == 4 {
|
||||
settings["profile"] = "high444"
|
||||
} else if sample.ColorSpace.ChromaSampling.J == 4 && sample.ColorSpace.ChromaSampling.A == 2 && sample.ColorSpace.ChromaSampling.B == 2 {
|
||||
settings["profile"] = "high422"
|
||||
} else if sample.ColorSpace.BitDepth > 8 {
|
||||
settings["profile"] = "high10"
|
||||
} else {
|
||||
settings["profile"] = "high"
|
||||
}
|
||||
|
||||
if encoder, err := NewEncoder(target, stream.Properties(), settings); err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -60,7 +97,7 @@ func TestEncode(t *testing.T) {
|
|||
defer wg.Done()
|
||||
for range s[0].Channel() {
|
||||
if encoded%10 == 0 {
|
||||
t.Logf("frame %d/1253", encoded)
|
||||
t.Logf("frame %d/%d", encoded, sample.Frames)
|
||||
}
|
||||
encoded++
|
||||
}
|
||||
|
@ -72,9 +109,33 @@ func TestEncode(t *testing.T) {
|
|||
|
||||
wg.Wait()
|
||||
|
||||
if encoded != 1253 {
|
||||
t.Fatalf("expected %d frames, got %d", 1253, encoded)
|
||||
if encoded != sample.Frames {
|
||||
t.Fatalf("expected %d frames, got %d", sample.Frames, encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncode_YUV420_8bit(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping encode test in short mode")
|
||||
}
|
||||
|
||||
testEncode(testdata.Y4M_Sintel_Trailer_720p24_YUV420_8bit, t)
|
||||
}
|
||||
|
||||
func TestEncode_YUV422_8bit(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping encode test in short mode")
|
||||
}
|
||||
|
||||
testEncode(testdata.Y4M_Ducks_Take_Off_720p50_YUV422_8bit, t)
|
||||
}
|
||||
|
||||
func TestEncode_YUV444_8bit(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping encode test in short mode")
|
||||
}
|
||||
|
||||
testEncode(testdata.Y4M_Ducks_Take_Off_720p50_YUV444_8bit, t)
|
||||
}
|
||||
|
|
|
@ -13,12 +13,18 @@ type Frame interface {
|
|||
Properties() Properties
|
||||
// PTS usually frame number
|
||||
PTS() int64
|
||||
|
||||
// Get16 get a pixel sample in 16-bit depth
|
||||
Get16(x, y int) (Y uint16, Cb uint16, Cr uint16)
|
||||
|
||||
// Get8 get a pixel sample in 8-bit depth
|
||||
Get8(x, y int) (Y uint8, Cb uint8, Cr uint8)
|
||||
}
|
||||
|
||||
type TypedFrame[T AllowedFrameTypes] interface {
|
||||
Frame
|
||||
|
||||
// GetNative get a pixel sample in native bit depth
|
||||
GetNative(x, y int) (Y T, Cb T, Cr T)
|
||||
|
||||
// GetNativeLuma also known as Y. Do not keep references to this slice, copy instead.
|
||||
|
|
1
go.mod
1
go.mod
|
@ -4,6 +4,7 @@ go 1.21
|
|||
|
||||
require (
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de
|
||||
)
|
||||
|
||||
|
|
2
go.sum
2
go.sum
|
@ -10,6 +10,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de h1:l5Za6utMv/HsBWWqzt4S8X17j+kt1uVETUX5UFhn2rE=
|
||||
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
|
1
testdata/.gitignore
vendored
1
testdata/.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
*.y4m
|
||||
*.y4m.xz
|
||||
*.ivf
|
14
testdata/prepare.sh
vendored
14
testdata/prepare.sh
vendored
|
@ -2,12 +2,22 @@
|
|||
|
||||
pushd "${0%/*}" || exit
|
||||
|
||||
# Samples taken from https://media.xiph.org/
|
||||
|
||||
if [[ ! -f "sintel_trailer_2k_720p24.y4m" ]]; then
|
||||
wget --show-progress -O "sintel_trailer_2k_720p24.y4m" https://git.gammaspectra.live/S.O.N.G/Video-Samples/media/branch/master/sintel_trailer_2k_720p24.y4m
|
||||
fi
|
||||
|
||||
if [[ ! -f "ducks_take_off_422_720p50.y4m" ]]; then
|
||||
wget --show-progress -O "ducks_take_off_422_720p50.y4m" https://git.gammaspectra.live/S.O.N.G/Video-Samples/media/branch/master/ducks_take_off_422_720p50.y4m
|
||||
fi
|
||||
|
||||
if [[ ! -f "ducks_take_off_444_720p50.y4m" ]]; then
|
||||
wget --show-progress -O "ducks_take_off_444_720p50.y4m" https://git.gammaspectra.live/S.O.N.G/Video-Samples/media/branch/master/ducks_take_off_444_720p50.y4m
|
||||
fi
|
||||
|
||||
if [[ ! -f "netflix_sol_levante_2160p24_12bit_av1_lossy.ivf" ]]; then
|
||||
wget --show-progress -O "sintel_trailer_2k_720p24_av1_low.ivf" https://git.gammaspectra.live/S.O.N.G/Video-Samples/media/branch/master/netflix_sol_levante_2160p24_12bit_av1_lossy.ivf
|
||||
fi
|
||||
|
||||
if [[ ! -f "sintel_trailer_2k_720p24_av1_low.ivf" ]]; then
|
||||
wget --show-progress -O "sintel_trailer_2k_720p24_av1_low.ivf" https://git.gammaspectra.live/S.O.N.G/Video-Samples/media/branch/master/sintel_trailer_2k_720p24_av1_low.ivf
|
||||
fi
|
84
testdata/testdata.go
vendored
84
testdata/testdata.go
vendored
|
@ -1,6 +1,7 @@
|
|||
package testdata
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/color"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
|
@ -16,7 +17,84 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
const (
|
||||
Y4M_Sintel_Trailer_720p24_YUV420_8bit = "testdata/sintel_trailer_2k_720p24.y4m"
|
||||
AV1_Sintel_Trailer_720p24_YUV420_8bit_Low = "testdata/sintel_trailer_2k_720p24_av1_low.ivf"
|
||||
type TestSample struct {
|
||||
Path string
|
||||
Url string
|
||||
Type string
|
||||
Width, Height int
|
||||
Frames int
|
||||
ColorSpace color.Space
|
||||
SkipNotFound bool
|
||||
}
|
||||
|
||||
var (
|
||||
Y4M_Sintel_Trailer_720p24_YUV420_8bit = TestSample{
|
||||
Path: "testdata/sintel_trailer_2k_720p24.y4m",
|
||||
// https://media.xiph.org/video/derf/y4m/sintel_trailer_2k_720p24.y4m
|
||||
Url: "https://git.gammaspectra.live/S.O.N.G/Video-Samples/media/branch/master/sintel_trailer_2k_720p24.y4m",
|
||||
Type: "y4m",
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Frames: 1253,
|
||||
ColorSpace: color.MustColorFormatFromString("420jpeg"),
|
||||
}
|
||||
Y4M_Big_Buck_Bunny_360p24_YUV420_8bit = TestSample{
|
||||
Path: "testdata/big_buck_bunny_360p24.y4m.xz",
|
||||
Url: "https://media.xiph.org/video/derf/y4m/big_buck_bunny_360p24.y4m.xz",
|
||||
Type: "y4m.xz",
|
||||
Width: 640,
|
||||
Height: 360,
|
||||
Frames: 14315,
|
||||
ColorSpace: color.MustColorFormatFromString("420jpeg"),
|
||||
SkipNotFound: true,
|
||||
}
|
||||
Y4M_Ducks_Take_Off_720p50_YUV444_8bit = TestSample{
|
||||
Path: "testdata/ducks_take_off_444_720p50.y4m",
|
||||
// https://media.xiph.org/video/derf/y4m/ducks_take_off_444_720p50.y4m
|
||||
Url: "https://git.gammaspectra.live/S.O.N.G/Video-Samples/media/branch/master/ducks_take_off_444_720p50.y4m",
|
||||
Type: "y4m",
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Frames: 500,
|
||||
ColorSpace: color.MustColorFormatFromString("444p8"),
|
||||
}
|
||||
Y4M_Ducks_Take_Off_720p50_YUV422_8bit = TestSample{
|
||||
Path: "testdata/ducks_take_off_422_720p50.y4m",
|
||||
// https://media.xiph.org/video/derf/y4m/ducks_take_off_422_720p50.y4m
|
||||
Url: "https://git.gammaspectra.live/S.O.N.G/Video-Samples/media/branch/master/ducks_take_off_422_720p50.y4m",
|
||||
Type: "y4m",
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Frames: 500,
|
||||
ColorSpace: color.MustColorFormatFromString("422p8"),
|
||||
}
|
||||
Y4M_Netflix_FoodMarket_2160p60_YUV420_10bit = TestSample{
|
||||
Path: "testdata/Netflix_FoodMarket_4096x2160_60fps_10bit_420.y4m",
|
||||
Url: "https://media.xiph.org/video/derf/ElFuente/Netflix_FoodMarket_4096x2160_60fps_10bit_420.y4m",
|
||||
Type: "y4m",
|
||||
Width: 4096,
|
||||
Height: 2160,
|
||||
Frames: 600,
|
||||
ColorSpace: color.MustColorFormatFromString("420p10"),
|
||||
SkipNotFound: true,
|
||||
}
|
||||
|
||||
AV1_Sintel_Trailer_720p24_YUV420_8bit_Low = TestSample{
|
||||
Path: "testdata/sintel_trailer_2k_720p24_av1_low.ivf",
|
||||
Url: "https://git.gammaspectra.live/S.O.N.G/Video-Samples/media/branch/master/sintel_trailer_2k_720p24_av1_low.ivf",
|
||||
Type: "ivf",
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Frames: 1253,
|
||||
ColorSpace: color.MustColorFormatFromString("420jpeg"),
|
||||
}
|
||||
AV1_Netflix_Sol_Levante_2160p24_YUV444_12bit_Lossy = TestSample{
|
||||
Path: "testdata/netflix_sol_levante_2160p24_12bit_av1_lossy.ivf",
|
||||
Url: "https://git.gammaspectra.live/S.O.N.G/Video-Samples/media/branch/master/netflix_sol_levante_2160p24_12bit_av1_lossy.ivf",
|
||||
Type: "ivf",
|
||||
Width: 3840,
|
||||
Height: 2160,
|
||||
Frames: 6313,
|
||||
ColorSpace: color.MustColorFormatFromString("444p12"),
|
||||
}
|
||||
)
|
||||
|
|
|
@ -19,12 +19,12 @@ func TestVMAFYUV420_8bit(t *testing.T) {
|
|||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
referenceFile, err := os.Open(testdata.Y4M_Sintel_Trailer_720p24_YUV420_8bit)
|
||||
referenceFile, err := os.Open(testdata.Y4M_Sintel_Trailer_720p24_YUV420_8bit.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer referenceFile.Close()
|
||||
distortedFile, err := os.Open(testdata.AV1_Sintel_Trailer_720p24_YUV420_8bit_Low)
|
||||
distortedFile, err := os.Open(testdata.AV1_Sintel_Trailer_720p24_YUV420_8bit_Low.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
39
utilities/settings.go
Normal file
39
utilities/settings.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package utilities
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetSettingString(m map[string]any, name string, fallback string) string {
|
||||
|
||||
v, ok := m[name]
|
||||
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
case bool:
|
||||
if val {
|
||||
return "1"
|
||||
} else {
|
||||
return "0"
|
||||
}
|
||||
case int:
|
||||
return strconv.FormatInt(int64(val), 10)
|
||||
case int64:
|
||||
return strconv.FormatInt(val, 10)
|
||||
case uint:
|
||||
return strconv.FormatUint(uint64(val), 10)
|
||||
case uint64:
|
||||
return strconv.FormatUint(val, 10)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(val), 'f', -1, 64)
|
||||
case float64:
|
||||
return strconv.FormatFloat(val, 'f', -1, 64)
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue