diff --git a/build/makefile_base.mak b/build/makefile_base.mak index 52d43aa2..f2637ad9 100644 --- a/build/makefile_base.mak +++ b/build/makefile_base.mak @@ -160,6 +160,7 @@ endif OPTIMIZE_FLAGS := -O2 -march=nocona $(call cc-option,$(CC),-mtune=core-avx2,) -mfpmath=sse SANITY_FLAGS := -fwrapv -fno-strict-aliasing COMMON_FLAGS := $(OPTIMIZE_FLAGS) $(SANITY_FLAGS) +CARGO_BUILD_ARG := --release # Use $(call QUOTE,$(VAR)) to flatten a list to a single element (for feeding to a shell) @@ -255,6 +256,10 @@ VKD3D := $(SRCDIR)/vkd3d-proton VKD3D_OBJ32 := ./obj-vkd3d32 VKD3D_OBJ64 := ./obj-vkd3d64 +MEDIACONV := $(SRCDIR)/media-converter +MEDIACONV_OBJ32 := ./obj-media-converter32 +MEDIACONV_OBJ64 := ./obj-media-converter64 + FONTS := $(SRCDIR)/fonts FONTS_OBJ := ./obj-fonts @@ -271,6 +276,7 @@ OBJ_DIRS := $(TOOLS_DIR32) $(TOOLS_DIR64) \ $(VRCLIENT_OBJ32) $(VRCLIENT_OBJ64) \ $(DXVK_OBJ32) $(DXVK_OBJ64) \ $(WINEWIDL_OBJ64) \ + $(MEDIACONV_OBJ32) $(MEDIACONV_OBJ64) \ $(VKD3D_OBJ32) $(VKD3D_OBJ64) $(OBJ_DIRS): @@ -425,7 +431,7 @@ $(DIST_FONTS): fonts ALL_TARGETS += dist GOAL_TARGETS += dist -dist: $(DIST_TARGETS) wine gst_good vrclient lsteamclient steam dxvk vkd3d-proton | $(DST_DIR) +dist: $(DIST_TARGETS) wine gst_good vrclient lsteamclient steam dxvk vkd3d-proton mediaconv | $(DST_DIR) echo `date '+%s'` `GIT_DIR=$(abspath $(SRCDIR)/.git) git describe --tags` > $(DIST_VERSION) cp $(DIST_VERSION) $(DST_BASE)/ rm -rf $(abspath $(DIST_PREFIX)) @@ -731,7 +737,6 @@ GST_GOOD_MESON_ARGS := \ -Dauparse=disabled \ -Dcairo=disabled \ -Dcutter=disabled \ - -Ddebugutils=disabled \ -Ddtmf=disabled \ -Deffectv=disabled \ -Dequalizer=disabled \ @@ -1069,12 +1074,11 @@ $(WINE_CONFIGURE_FILES64): $(MAKEFILE_DEP) | faudio64 gst_base64 $(WINE_OBJ64) --enable-win64 \ --disable-tests \ --prefix=$(abspath $(DST_DIR)) \ + LD_LIBRARY_PATH=$(abspath $(TOOLS_DIR64))/lib \ STRIP=$(STRIP_QUOTED) \ CFLAGS="-I$(abspath $(TOOLS_DIR64))/include -g $(COMMON_FLAGS)" \ - CXXFLAGS="-I$(abspath $(TOOLS_DIR64))/include -g $(COMMON_FLAGS) -std=c++17" \ LDFLAGS=-L$(abspath $(TOOLS_DIR64))/lib \ PKG_CONFIG_PATH=$(abspath $(TOOLS_DIR64))/lib/pkgconfig \ - LD_LIBRARY_PATH=$(abspath $(TOOLS_DIR64))/lib \ CC=$(CC_QUOTED) \ CXX=$(CXX_QUOTED) \ CROSSDEBUG=split-dwarf @@ -1087,12 +1091,11 @@ $(WINE_CONFIGURE_FILES32): $(MAKEFILE_DEP) | faudio32 gst_base32 $(WINE_OBJ32) --without-curses \ --disable-tests \ --prefix=$(abspath $(WINE_DST32)) \ + LD_LIBRARY_PATH=$(abspath $(TOOLS_DIR32))/lib \ STRIP=$(STRIP_QUOTED) \ CFLAGS="-I$(abspath $(TOOLS_DIR32))/include -g $(COMMON_FLAGS)" \ - CXXFLAGS="-I$(abspath $(TOOLS_DIR32))/include -g $(COMMON_FLAGS) -std=c++17" \ LDFLAGS=-L$(abspath $(TOOLS_DIR32))/lib \ PKG_CONFIG_PATH=$(abspath $(TOOLS_DIR32))/lib/pkgconfig \ - LD_LIBRARY_PATH=$(abspath $(TOOLS_DIR32))/lib \ CC=$(CC_QUOTED) \ CXX=$(CXX_QUOTED) \ PKG_CONFIG="$(PKG_CONFIG32)" \ @@ -1383,6 +1386,23 @@ vkd3d-proton: vkd3d32 vkd3d64 # build_vrclient64_tests # build_vrclient32_tests +mediaconv32: SHELL = $(CONTAINER_SHELL32) +mediaconv32: $(MAKEFILE_DEP) gstreamer32 | $(MEDIACONV_OBJ32) + cd $(abspath $(MEDIACONV)) && \ + PKG_CONFIG_ALLOW_CROSS=1 \ + PKG_CONFIG_PATH=$(abspath $(TOOLS_DIR32))/lib/pkgconfig \ + cargo build --target i686-unknown-linux-gnu --target-dir $(abspath $(MEDIACONV_OBJ32)) $(CARGO_BUILD_ARG) + cp -a $(abspath $(MEDIACONV_OBJ32))/i686-unknown-linux-gnu/release/libprotonmediaconverter.so $(abspath $(DST_DIR))/lib/gstreamer-1.0/ + +mediaconv64: SHELL = $(CONTAINER_SHELL64) +mediaconv64: $(MAKEFILE_DEP) gstreamer64 | $(MEDIACONV_OBJ64) + cd $(abspath $(MEDIACONV)) && \ + PKG_CONFIG_PATH=$(abspath $(TOOLS_DIR64))/lib/pkgconfig \ + cargo build --target x86_64-unknown-linux-gnu --target-dir $(abspath $(MEDIACONV_OBJ64)) $(CARGO_BUILD_ARG) + cp -a $(abspath $(MEDIACONV_OBJ64))/x86_64-unknown-linux-gnu/release/libprotonmediaconverter.so $(abspath $(DST_DIR))/lib64/gstreamer-1.0/ + +mediaconv: mediaconv32 mediaconv64 + ALL_TARGETS += fonts GOAL_TARGETS += fonts diff --git a/dist.LICENSE b/dist.LICENSE index 12b71f08..017bc505 100644 --- a/dist.LICENSE +++ b/dist.LICENSE @@ -243,6 +243,116 @@ Visit orc at ---- ---- ---- ---- +This software contains the following Rust libraries under the MIT license: + + gstreamer-rs + + array-init + Copyright (c) 2010 The Rust Project Developers + + autocfg + Copyright (c) 2018 Josh Stone + + bitflags + num-integer + num-rational + num-traits + Copyright (c) 2014 The Rust Project Developers + + cfg-if + pkg-config + proc-macro2 + toml + Copyright (c) 2014 Alex Crichton + + chrono + Copyright (c) 2014 Kang Seonghoon + + crc32fast + Copyright (c) 2018 Sam Rijs, Alex Crichton and contributors + + futures-channel + futures-core + futures-executor + futures-macro + futures-task + futures-util + Copyright (c) 2016 Alex Crichton + Copyright (c) 2017 The Tokio Authors + + glib + Copyright (c) 2013-2015 The Gtk-rs Project Developers + + heck + unicode-segmentation + unicode-xid + Copyright (c) 2015 The Rust Project Developers + + libc + Copyright (c) 2014-2020 The Rust Project Developers + + muldiv + Copyright (c) 2017 Sebastian Dröge + + murmur3 + Copyright (c) 2016 Stu Small + + pin-utils + Copyright (c) 2018 The pin-utils authors + + pretty-hex + Copyright (c) 2018 Andrei Volnin + + proc-macro-error + Copyright (c) 2019-2020 CreepySkeleton + + proc-macro-hack + Copyright (c) 2018 David Tolnay + + quote + Copyright (c) 2016 The Rust Project Developers + + slab + Copyright (c) 2019 Carl Lerche + + time + Copyright (c) 2019 Jacob Pratt + + version_check + Copyright (c) 2017-2018 Sergio Benitez + + anyhow + either + itertools + once_cell + paste + pin-project + proc-macro-crate + serde + syn + syn-mid + thiserror + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +---- ---- ---- ---- + Parts of this software are based on the OpenVR SDK, which is Copyright (c) 2015, Valve Corporation diff --git a/media-converter/Cargo.toml b/media-converter/Cargo.toml new file mode 100644 index 00000000..8a379868 --- /dev/null +++ b/media-converter/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "proton-media-converter" +version = "1.0.0" +authors = ["Andrew Eikum "] +repository = "https://github.com/ValveSoftware/Proton/" +license = "MIT/Apache-2.0" +edition = "2018" +description = "Proton media converter" + +[dependencies] +glib = "0.10.1" +gstreamer = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", tag = "0.16.2" } +gstreamer-base = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", tag = "0.16.2" } +gstreamer-video = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", tag = "0.16.2" } +gstreamer-audio = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", tag = "0.16.2" } +once_cell = "1.0" +crc32fast = "1.2" + +[lib] +name = "protonmediaconverter" +crate-type = ["cdylib"] +path = "src/lib.rs" + +[build-dependencies] +gst-plugin-version-helper = { git = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs", tag = "0.6.0" } + +[profile.release] +lto = true +opt-level = 3 +debug = false +panic = 'unwind' + +[profile.dev] +opt-level = 1 + +#if you need local modifications to gstreamer-rs you can point to it here +#[patch.'https://gitlab.freedesktop.org/gstreamer/gstreamer-rs'] +#gstreamer = { path = "../gstreamer-rs/gstreamer" } +#gstreamer-base = { path = "../gstreamer-rs/gstreamer-base" } +#gstreamer-video = { path = "../gstreamer-rs/gstreamer-video" } +#gstreamer-audio = { path = "../gstreamer-rs/gstreamer-audio" } diff --git a/media-converter/Makefile b/media-converter/Makefile new file mode 100644 index 00000000..e94f3fe2 --- /dev/null +++ b/media-converter/Makefile @@ -0,0 +1,38 @@ +SONAME := libprotonmediaconverter.so + +TRIPLE64 := x86_64-unknown-linux-gnu +TRIPLE32 := i686-unknown-linux-gnu + +DESTDIR ?= dist/ +DIST_GST_DIR64 := $(DESTDIR)/lib64/gstreamer-1.0/ +DIST_GST_DIR32 := $(DESTDIR)/lib/gstreamer-1.0/ + +ifeq ($(DEBUG),) + CARGO_RELEASE_ARG := --release + TARGET_BUILD_TYPE := release +else + CARGO_RELEASE_ARG := + TARGET_BUILD_TYPE := debug +endif + +all: install + +build: blank.ogv blank.ptna + cargo build --target $(TRIPLE64) $(CARGO_RELEASE_ARG) + PKG_CONFIG_ALLOW_CROSS=1 cargo build --target $(TRIPLE32) $(CARGO_RELEASE_ARG) + +install: build + install -d "$(DIST_GST_DIR64)" + install -m 755 target/$(TRIPLE64)/$(TARGET_BUILD_TYPE)/$(SONAME) "$(DIST_GST_DIR64)" + install -d "$(DIST_GST_DIR32)" + install -m 755 target/$(TRIPLE32)/$(TARGET_BUILD_TYPE)/$(SONAME) "$(DIST_GST_DIR32)" + +blank.ogv: + #120 frames @ 30 FPS == 4 seconds + gst-launch-1.0 videotestsrc num-buffers=120 pattern=smpte ! theoraenc ! oggmux ! filesink location=blank.ogv + +make_blank_ptna: make_blank_ptna.c + gcc -Wall -O2 $(shell pkg-config --cflags opus) -o $@ $< -lm $(shell pkg-config --libs opus) + +blank.ptna: make_blank_ptna + ./make_blank_ptna $@ diff --git a/media-converter/README b/media-converter/README new file mode 100644 index 00000000..b87a25fb --- /dev/null +++ b/media-converter/README @@ -0,0 +1,12 @@ +This module is a gstreamer plugin which provides the ability to replace media +data encoded in certain formats with media encoded in another format. There +are two main components, `videoconv` for converting video data provided to +Quartz and Media Foundation, and `audioconv` for converting audio data +provided to XAudio2. + +The broad idea is to hash the incoming data and replace it with data looked up +from a cache. If there is a cache miss, then the data is recorded to disk and +instead replaced by "blank" media. The conversion should be transparent to the +application (Wine, FAudio) so no changes are required to the application. + +See the accompanying source files for more information. diff --git a/media-converter/blank.ogv b/media-converter/blank.ogv new file mode 100644 index 00000000..79aff93b Binary files /dev/null and b/media-converter/blank.ogv differ diff --git a/media-converter/blank.ptna b/media-converter/blank.ptna new file mode 100644 index 00000000..44af1343 Binary files /dev/null and b/media-converter/blank.ptna differ diff --git a/media-converter/build.rs b/media-converter/build.rs new file mode 100644 index 00000000..0d1ddb61 --- /dev/null +++ b/media-converter/build.rs @@ -0,0 +1,5 @@ +extern crate gst_plugin_version_helper; + +fn main() { + gst_plugin_version_helper::get_info() +} diff --git a/media-converter/make_blank_ptna.c b/media-converter/make_blank_ptna.c new file mode 100644 index 00000000..ace3de7f --- /dev/null +++ b/media-converter/make_blank_ptna.c @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2020, Valve Corporation + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* Dumps a "blank" Proton Audio sample. For documentation of the Proton Audio + * format, see audioconv.rs. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(*x)) + +/* indexes into frame_sizes[] */ +#define FRAME_2p5MS 0 +#define FRAME_SMALLEST FRAME_2p5MS +#define FRAME_5MS 1 +#define FRAME_10MS 2 +#define FRAME_20MS 3 +#define FRAME_40MS 4 +#define FRAME_60MS 5 +#define FRAME_LARGEST FRAME_60MS + +/* sample properties */ +static const int SAMPLERATE = 48000; +static const int CHANNELS = 1; +static const int SAMPLE_LENGTH = FRAME_10MS; /* must match value in audioconv.rs */ +static const float AUDIBLE_HZ = 400.f; /* freq for the --audible switch */ + +static const int frame_sizes[] = { + SAMPLERATE * 2.5 / 1000, + SAMPLERATE * 5 / 1000, + SAMPLERATE * 10 / 1000, + SAMPLERATE * 20 / 1000, + SAMPLERATE * 40 / 1000, + SAMPLERATE * 60 / 1000, +}; + +static float theta = 0.f; + +static const uint32_t FLAG_HEADER = 0x10000000; +static const uint32_t AUDIOCONV_PADDING_LENGTH_SHIFT = 12; + +static int complete_write(const int file, const void *buf, const size_t len) +{ + size_t written = 0; + ssize_t ret; + while(written < len){ + ret = write(file, ((const char *)buf) + written, len - written); + if(ret < 0){ + if(errno != EINTR && errno != EAGAIN) + return 0; + }else + written += ret; + } + return written == len; +} + +static int dump_hz(float hz, int samples, OpusEncoder *encoder, int outfile) +{ + int i = 0, j, c, frame_size, tot = 0, padding; + opus_int32 written; + uint32_t pkt_header; + float val, *vals; + unsigned char packet[4000 /* suggested by opus_encoder(3) */ ]; + + fprintf(stdout, "dumping %u samples at %f hz\n", samples, hz); + + vals = malloc(sizeof(float) * CHANNELS * frame_sizes[FRAME_LARGEST]); + + while(i < samples){ + + /* find the largest packet we can write with the samples remaining */ + frame_size = -1; + for(j = ARRAY_SIZE(frame_sizes) - 1; j >= 0; --j){ + if(samples - i >= frame_sizes[j]){ + frame_size = frame_sizes[j]; + break; + } + } + + if(frame_size < 0){ + /* couldn't fill a whole packet, so write a partial */ + frame_size = frame_sizes[FRAME_SMALLEST]; + padding = frame_size - (samples - i); + }else + padding = 0; + + for(j = 0; j < frame_size - padding; ++j){ + val = sinf(theta); + for(c = 0; c < CHANNELS; ++c) + vals[j * CHANNELS + c] = val; + theta += hz * 2.f * M_PI / (float)SAMPLERATE; + while(theta >= 2.f * M_PI) + theta -= 2.f * M_PI; + } + + if(padding) + memset(vals + (frame_size - padding) * CHANNELS, 0, sizeof(float) * padding * CHANNELS); + + written = opus_encode_float(encoder, vals, frame_size, packet, sizeof(packet)); + if(written < 0){ + fprintf(stderr, "fatal: opus_encode failed!!!\n"); + free(vals); + return 0; + } + + pkt_header = written | (padding << AUDIOCONV_PADDING_LENGTH_SHIFT); + + fprintf(stdout, "encoded %u samples (%u are padding) to %u bytes\n", frame_size, padding, written); + if(!complete_write(outfile, &pkt_header, sizeof(pkt_header))){ + fprintf(stderr, "fatal: error writing packet header!!!\n"); + free(vals); + return 0; + } + if(!complete_write(outfile, packet, written)){ + fprintf(stderr, "fatal: error writing packet contents!!!\n"); + free(vals); + return 0; + } + tot += written + sizeof(written); + + i += frame_size; + } + + fprintf(stdout, "done dumping, %u bytes\n", tot); + + free(vals); + + return 1; +} + +/* OGG's opus header (from RFC 7845 Section 5.1) */ +struct __attribute__((__packed__)) opus_header { + uint64_t magic; + uint8_t version; + uint8_t channels; + uint16_t preskip; + uint32_t input_samplerate; + uint16_t output_gain; + uint8_t mapping_family; +}; + +static int dump_header(int outfile) +{ + struct opus_header header; + uint32_t sz = sizeof(header) | FLAG_HEADER; + + static const char magic[] = "OpusHead"; + + memcpy(&header.magic, magic, sizeof(magic)); + header.version = 1; + header.channels = CHANNELS; + header.preskip = 0; + header.input_samplerate = SAMPLERATE; + header.output_gain = 0; + header.mapping_family = 0; /* FIXME: we need to support mc */ + + if(!complete_write(outfile, &sz, sizeof(sz))){ + fprintf(stderr, "fatal: error writing opus header header!!!\n"); + return 0; + } + + if(!complete_write(outfile, &header, sizeof(header))){ + fprintf(stderr, "fatal: error writing opus header!!!\n"); + return 0; + } + + return 1; +} + +void usage(const char *name) +{ + fprintf(stderr, "usage:\n"); + fprintf(stderr, "\t%s [--help|-h] [--audible] \n", name); + fprintf(stderr, "\n"); + fprintf(stderr, "\t--audible\tGenerate an audible sound clip instead of silence.\n"); + fprintf(stderr, "\t--help -h\tPrint this help message.\n\n"); +} + +int main(int argc, char **argv) +{ + int err, outfile, i, audible = 0; + OpusEncoder *encoder; + const char *outfile_name = NULL; + + for(i = 1; i < argc; ++i){ + if(!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")){ + usage(argv[0]); + return 0; + }else if(!strcmp(argv[i], "--audible")){ + audible = 1; + }else if(outfile_name){ + fprintf(stderr, "error: too many arguments.\n\n"); + usage(argv[0]); + return 1; + }else{ + outfile_name = argv[i]; + } + } + + if(!outfile_name){ + fprintf(stderr, "error: missing filename.\n\n"); + usage(argv[0]); + return 1; + } + + encoder = opus_encoder_create(SAMPLERATE, CHANNELS, OPUS_APPLICATION_AUDIO, &err); + if(!encoder){ + fprintf(stderr, "couldn't create opus encoder, err: 0x%x\n", err); + return 1; + } + + outfile = open(outfile_name, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if(outfile < 0){ + fprintf(stderr, "couldn't open file \"%s\": %s\n", outfile_name, strerror(errno)); + opus_encoder_destroy(encoder); + return 1; + } + + if(!dump_header(outfile)){ + close(outfile); + unlink(outfile_name); + opus_encoder_destroy(encoder); + return 1; + } + + if(!dump_hz(audible ? AUDIBLE_HZ : 0.f, frame_sizes[SAMPLE_LENGTH], encoder, outfile)){ + close(outfile); + unlink(outfile_name); + opus_encoder_destroy(encoder); + return 1; + } + + close(outfile); + opus_encoder_destroy(encoder); + + return 0; +} diff --git a/media-converter/src/audioconv.rs b/media-converter/src/audioconv.rs new file mode 100644 index 00000000..e845eb2b --- /dev/null +++ b/media-converter/src/audioconv.rs @@ -0,0 +1,882 @@ +/* + * Copyright (c) 2020 Valve Corporation + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use glib; +use glib::subclass; +use glib::subclass::prelude::*; + +use crate::format_hash; +use crate::HASH_SEED; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::EventView; +use gst::QueryView; +use gst_audio; + +use std::sync::Mutex; +use std::io; +use std::io::Read; +use std::fs; +use std::fs::OpenOptions; + +#[cfg(target_arch = "x86")] +use crate::murmur3_x86_128::murmur3_x86_128_full as murmur3_128_full; +#[cfg(target_arch = "x86")] +use crate::murmur3_x86_128::murmur3_x86_128_state as murmur3_128_state; + +#[cfg(target_arch = "x86_64")] +use crate::murmur3_x64_128::murmur3_x64_128_full as murmur3_128_full; +#[cfg(target_arch = "x86_64")] +use crate::murmur3_x64_128::murmur3_x64_128_state as murmur3_128_state; + +use crate::fossilize; +use crate::copy_into_array; +use crate::BufferedReader; + +use once_cell::sync::Lazy; + +/* Algorithm + * --------- + * + * The application feeds encoded audio into XAudio2 in chunks. Since we don't have access to all + * chunks in a stream on initialization (as we do with the video converter), we continuously hash + * the stream as it is sent to us. Each "chunk" is identified as the hash of the entire stream up + * to that chunk. + * + * Since chunks are small (~2 kilobytes), this leads to a significant possibility of two different + * streams having identical intro chunks (imagine two streams that start with several seconds of + * silence). This means we need a tree of chunks. Suppose two incoming streams with chunks that + * hash as shown (i.e. identical intro chunks that diverge later): + * + * Stream 1: [AA BB CC DD] + * + * Stream 2: [AA BB YY ZZ] + * + * We record a tree and the transcoder will walk it depth-first in order to reconstruct each unique + * stream: + * + * AA => aa.ptna + * AA+BB => bb.ptna + * AA+BB+CC => cc.ptna + * AA+BB+CC+DD => dd.ptna + * AA+BB+YY => yy.ptna + * AA+BB+YY+ZZ => zz.ptna + * + * Upon playback, we chain each transcoded stream chunk together as the packets come in: + * + * AA -> start stream with aa.ptna + * BB -> play bb.ptna + * CC -> play cc.ptna + * DD -> play dd.ptna + * + * or: + * + * AA -> start stream with aa.ptna + * BB -> play bb.ptna + * YY -> play yy.ptna + * ZZ -> play zz.ptna + * + * or: + * + * AA -> start stream with aa.ptna + * NN -> not recognized, instead play blank.ptna and mark this stream as needs-transcoding + * OO -> play blank.ptna + * PP -> play blank.ptna + * When the Stream is destroyed, we'll record AA+NN+OO+PP into the needs-transcode database + * for the transcoder to convert later. + * + * + * Physical Format + * --------------- + * + * All stored values are little-endian. + * + * Transcoded audio is stored in the "transcoded" Fossilize database under the + * AUDIOCONV_FOZ_TAG_PTNADATA tag. Each chunk is stored in one entry with as many of the following + * "Proton Audio" (ptna) packets as are required to store the entire transcoded chunk: + * + * uint32_t packet_header: Information about the upcoming packet, see bitmask: + * MSB [FFFF PPPP PPPP PPPP PPPP LLLL LLLL LLLL] LSB + * L: Number of _bytes_ in this packet following this header. + * P: Number of _samples_ at the end of this packet which are padding and should be skipped. + * F: Flag bits: + * 0x1: This packet is an Opus header + * 0x2, 0x4, 0x8: Reserved for future use. + * + * If the Opus header flag is set: + * Following packet is an Opus identification header, as defined in RFC 7845 "Ogg + * Encapsulation for the Opus Audio Codec" Section 5.1. + * + * + * If the header flag is not set: + * Following packet is raw Opus data to be sent to an Opus decoder. + * + * + * If we encounter a stream which needs transcoding, we record the buffers and metadata in + * a Fossilize database. The database has three tag types: + * + * AUDIOCONV_FOZ_TAG_STREAM: This identifies each unique stream of buffers. For example: + * [hash(AA+BB+CC+DD)] -> [AA, BB, CC, DD] + * [hash(AA+BB+XX+YY)] -> [AA, BB, XX, YY] + * + * AUDIOCONV_FOZ_TAG_AUDIODATA: This contans the actual encoded audio data. For example: + * [AA] -> [AA's buffer data] + * [BB] -> [BB's buffer data] + * + * AUDIOCONV_FOZ_TAG_CODECINFO: This contans the codec data required to decode the buffer. Only + * the "head" of each stream is recorded. For example: + * [AA] -> [ + * uint32_t wmaversion (from WAVEFORMATEX.wFormatTag) + * uint32_t bitrate (from WAVEFORMATEX.nAvgBytesPerSec) + * uint32_t channels (WAVEFORMATEX.nChannels) + * uint32_t rate (WAVEFORMATEX.nSamplesPerSec) + * uint32_t block_align (WAVEFORMATEX.nBlockAlign) + * uint32_t depth (WAVEFORMATEX.wBitsPerSample) + * char[remainder of entry] codec_data (codec data which follows WAVEFORMATEX) + * ] + * + */ + +const AUDIOCONV_ENCODED_LENGTH_MASK: u32 = 0x00000fff; /* 4kB fits in here */ +const AUDIOCONV_PADDING_LENGTH_MASK: u32 = 0x0ffff000; /* 120ms of samples at 48kHz fits in here */ +const AUDIOCONV_PADDING_LENGTH_SHIFT: u32 = 12; +const AUDIOCONV_FLAG_MASK: u32 = 0xf0000000; +const AUDIOCONV_FLAG_HEADER: u32 = 0x10000000; /* this chunk is the Opus header */ +const _AUDIOCONV_FLAG_RESERVED1: u32 = 0x20000000; /* not yet used */ +const _AUDIOCONV_FLAG_RESERVED2: u32 = 0x40000000; /* not yet used */ +const _AUDIOCONV_FLAG_V2: u32 = 0x80000000; /* indicates a "version 2" header, process somehow differently (TBD) */ + +/* properties of the "blank" audio file */ +const BLANK_AUDIO_FILE_LENGTH_MS: f32 = 10.0; +const BLANK_AUDIO_FILE_RATE: f32 = 48000.0; + +static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "protonaudioconverter", + gst::DebugColorFlags::empty(), + Some("Proton audio converter")) +}); + +static DUMP_FOZDB: Lazy>> = Lazy::new(|| { + let dump_file_path = match std::env::var("MEDIACONV_AUDIO_DUMP_FILE") { + Err(_) => { return Mutex::new(None); }, + Ok(c) => c, + }; + + let dump_file_path = std::path::Path::new(&dump_file_path); + + if fs::create_dir_all(&dump_file_path.parent().unwrap()).is_err() { + return Mutex::new(None); + } + + match fossilize::StreamArchive::new(&dump_file_path, OpenOptions::new().write(true).read(true).create(true), AUDIOCONV_FOZ_NUM_TAGS) { + Ok(newdb) => Mutex::new(Some(newdb)), + Err(_) => Mutex::new(None), + } +}); + +#[derive(Clone)] +struct NeedTranscodeHead { + wmaversion: i32, + bitrate: i32, + channels: i32, + rate: i32, + block_align: i32, + depth: i32, + codec_data: Vec +} + +impl NeedTranscodeHead { + fn new_from_caps(caps: &gst::CapsRef) -> Result { + let s = caps.get_structure(0).ok_or(gst_loggable_error!(CAT, "Caps have no WMA data!"))?; + + let wmaversion = s.get_some::("wmaversion").map_err(|_| gst_loggable_error!(CAT, "Caps have no wmaversion field"))?; + let bitrate = s.get_some::("bitrate").map_err(|_| gst_loggable_error!(CAT, "Caps have no bitrate field"))?; + let channels = s.get_some::("channels").map_err(|_| gst_loggable_error!(CAT, "Caps have no channels field"))?; + let rate = s.get_some::("rate").map_err(|_| gst_loggable_error!(CAT, "Caps have no rate field"))?; + let block_align = s.get_some::("block_align").map_err(|_| gst_loggable_error!(CAT, "Caps have no block_align field"))?; + let depth = s.get_some::("depth").map_err(|_| gst_loggable_error!(CAT, "Caps have no depth field"))?; + let codec_data_buf = s.get::("codec_data") + .map_err(|_| gst_loggable_error!(CAT, "Caps have no codec_data field"))? + .ok_or(gst_loggable_error!(CAT, "Caps have NULL codec_data field"))?; + + let mapped = codec_data_buf.into_mapped_buffer_readable().unwrap(); + let mut codec_data = Vec::new(); + codec_data.extend_from_slice(mapped.as_slice()); + + Ok(NeedTranscodeHead { + wmaversion, + bitrate, + channels, + rate, + block_align, + depth, + codec_data, + }) + } + + fn serialize(&self) -> Vec { + let mut ret = Vec::new(); + ret.extend_from_slice(&self.wmaversion.to_le_bytes()); + ret.extend_from_slice(&self.bitrate.to_le_bytes()); + ret.extend_from_slice(&self.channels.to_le_bytes()); + ret.extend_from_slice(&self.rate.to_le_bytes()); + ret.extend_from_slice(&self.block_align.to_le_bytes()); + ret.extend_from_slice(&self.depth.to_le_bytes()); + ret.extend(self.codec_data.iter()); + ret + } +} + +const AUDIOCONV_FOZ_TAG_STREAM: u32 = 0; +const AUDIOCONV_FOZ_TAG_CODECINFO: u32 = 1; +const AUDIOCONV_FOZ_TAG_AUDIODATA: u32 = 2; +const AUDIOCONV_FOZ_TAG_PTNADATA: u32 = 3; +const AUDIOCONV_FOZ_NUM_TAGS: usize = 4; + +/* represents a Stream, a sequence of buffers */ +struct StreamState { + hash_state: murmur3_128_state, + cur_hash: u128, + buffers: Vec<(u128, gst::MappedBuffer)>, + loop_buffers: Vec<(u128, gst::MappedBuffer)>, + codec_info: Option, + needs_dump: bool, +} + +enum LoopState { + NoLoop, + Looping, + LoopEnded, +} + +impl StreamState { + fn new() -> Self { + Self { + hash_state: murmur3_128_state::new(HASH_SEED), + buffers: Vec::<(u128, gst::MappedBuffer)>::new(), + loop_buffers: Vec::<(u128, gst::MappedBuffer)>::new(), + cur_hash: 0, + codec_info: None, + needs_dump: false, + } + } + + fn record_buffer(&mut self, buf_hash: u128, loop_hash: u128, buffer: gst::MappedBuffer, codec_info: Option<&NeedTranscodeHead>) -> io::Result { + if self.codec_info.is_none() { + if let Some(codec_info) = codec_info { + self.codec_info = Some(codec_info.clone()); + } + } + + if self.loop_buffers.len() < self.buffers.len() && + self.buffers[self.loop_buffers.len()].0 == loop_hash { + + self.loop_buffers.push((buf_hash /* not loop_hash! */, buffer)); + + if self.loop_buffers.len() == self.buffers.len() { + /* full loop, just drop them */ + self.loop_buffers.clear(); + return Ok(LoopState::LoopEnded); + } + + Ok(LoopState::Looping) + }else{ + if self.loop_buffers.len() > 0 { + /* partial loop, track them and then continue */ + self.buffers.append(&mut self.loop_buffers); + } + + self.buffers.push((buf_hash, buffer)); + + self.cur_hash = murmur3_128_full(&mut (&buf_hash.to_le_bytes() as &[u8]), &mut self.hash_state)?; + Ok(LoopState::NoLoop) + } + } + + fn write_to_foz(&self) -> Result<(), gst::LoggableError> { + if self.needs_dump && self.buffers.len() > 0 { + let mut db = (*DUMP_FOZDB).lock().unwrap(); + let db = match &mut *db { + Some(d) => d, + None => { return Err(gst_loggable_error!(CAT, "Failed to open fossilize db!")) }, + }; + + let mut found = db.has_entry(AUDIOCONV_FOZ_TAG_STREAM, self.cur_hash); + + if !found { + /* are there any recorded streams of which this stream is a subset? */ + let stream_ids = db.iter_tag(AUDIOCONV_FOZ_TAG_STREAM).cloned().collect::>(); + + found = stream_ids.iter().find(|stream_id| { + let mut offs = 0; + + for cur_buf_id in self.buffers.iter() { + let mut buf = [0u8; 16]; + + let res = db.read_entry(AUDIOCONV_FOZ_TAG_STREAM, **stream_id, offs, &mut buf, fossilize::CRCCheck::WithCRC); + + let buffer_id = match res { + Err(_) => { return false; } + Ok(s) => { + if s != std::mem::size_of::() { + return false; + } + u128::from_le_bytes(buf) + } + }; + + if buffer_id != (*cur_buf_id).0 { + return false; + } + + offs += 16; + } + + gst_trace!(CAT, "stream id {} is a subset of {}, so not recording stream", self.cur_hash, **stream_id); + return true; + }).is_some(); + } + + if !found { + gst_trace!(CAT, "recording stream id {}", self.cur_hash); + db.write_entry(AUDIOCONV_FOZ_TAG_CODECINFO, + self.buffers[0].0, + &mut self.codec_info.as_ref().unwrap().serialize().as_slice(), + fossilize::CRCCheck::WithCRC) + .map_err(|e| gst_loggable_error!(CAT, "Unable to write stream header: {:?}", e))?; + + db.write_entry(AUDIOCONV_FOZ_TAG_STREAM, + self.cur_hash, + &mut StreamStateSerializer::new(self), + fossilize::CRCCheck::WithCRC) + .map_err(|e| gst_loggable_error!(CAT, "Unable to write stream: {:?}", e))?; + + for buffer in self.buffers.iter() { + db.write_entry(AUDIOCONV_FOZ_TAG_AUDIODATA, + buffer.0, + &mut buffer.1.as_slice(), + fossilize::CRCCheck::WithCRC) + .map_err(|e| gst_loggable_error!(CAT, "Unable to write audio data: {:?}", e))?; + } + } + } + Ok(()) + } + + fn reset(&mut self) { + self.hash_state.reset(); + self.buffers.clear(); + self.loop_buffers.clear(); + self.cur_hash = 0; + self.codec_info = None; + self.needs_dump = false; + } +} + +struct StreamStateSerializer<'a> { + stream_state: &'a StreamState, + cur_idx: usize, +} + +impl<'a> StreamStateSerializer<'a> { + fn new(stream_state: &'a StreamState) -> Self { + StreamStateSerializer { + stream_state, + cur_idx: 0 + } + } +} + +impl<'a> Read for StreamStateSerializer<'a> { + fn read(&mut self, out: &mut [u8]) -> io::Result { + if self.cur_idx >= self.stream_state.buffers.len() { + return Ok(0); + } + + out[0..std::mem::size_of::()].copy_from_slice(&self.stream_state.buffers[self.cur_idx].0.to_le_bytes()); + self.cur_idx += 1; + + Ok(std::mem::size_of::()) + } +} + +fn hash_data(dat: &[u8], len: usize, hash_state: &mut murmur3_128_state) -> io::Result { + murmur3_128_full(&mut BufferedReader::new(dat, len), hash_state) +} + +struct AudioConvState { + sent_header: bool, + + codec_data: Option, + + hash_state: murmur3_128_state, + loop_hash_state: murmur3_128_state, + + stream_state: StreamState, + + read_fozdb: Option, +} + +impl AudioConvState { + fn new() -> Result { + + let read_fozdb_path = std::env::var("MEDIACONV_AUDIO_TRANSCODED_FILE").map_err(|_| { + gst_loggable_error!(CAT, "MEDIACONV_AUDIO_TRANSCODED_FILE is not set!") + })?; + + let read_fozdb = match fossilize::StreamArchive::new(&read_fozdb_path, OpenOptions::new().read(true), AUDIOCONV_FOZ_NUM_TAGS) { + Ok(s) => Some(s), + Err(_) => None, + }; + + Ok(AudioConvState { + sent_header: false, + + codec_data: None, + + hash_state: murmur3_128_state::new(HASH_SEED), + loop_hash_state: murmur3_128_state::new(HASH_SEED), + + stream_state: StreamState::new(), + + read_fozdb, + }) + } + + fn reset(&mut self) { + if let Err(e) = self.stream_state.write_to_foz() { + e.log(); + } + + self.stream_state.reset(); + + self.hash_state.reset(); + self.loop_hash_state.reset(); + } + + fn open_transcode_file(&mut self, buffer: gst::Buffer) -> io::Result<(Box<[u8]>, f32)> { + let mapped = buffer.into_mapped_buffer_readable().unwrap(); + let buf_len = mapped.get_size(); + + let hash = hash_data(mapped.as_slice(), buf_len, &mut self.hash_state) + .map_err(|e|{ gst_warning!(CAT, "Hashing buffer failed! {}", e); io::ErrorKind::Other })?; + let loop_hash = hash_data(mapped.as_slice(), buf_len, &mut self.loop_hash_state) + .map_err(|e|{ gst_warning!(CAT, "Hashing buffer failed! {}", e); io::ErrorKind::Other })?; + + let try_loop = match self.stream_state.record_buffer(hash, loop_hash, mapped, Some(self.codec_data.as_ref().unwrap()))? { + LoopState::NoLoop => { self.loop_hash_state.reset(); false }, + LoopState::Looping => { true }, + LoopState::LoopEnded => { self.loop_hash_state.reset(); true }, + }; + + if try_loop { + gst_log!(CAT, "Buffer hash: {} (loop: {})", format_hash(hash), format_hash(loop_hash)); + }else{ + gst_log!(CAT, "Buffer hash: {}", format_hash(hash)); + } + + /* try to read transcoded data */ + if let Some(read_fozdb) = &mut self.read_fozdb { + if let Ok(transcoded_size) = read_fozdb.entry_size(AUDIOCONV_FOZ_TAG_PTNADATA, hash) { + /* success */ + let mut buf = vec![0u8; transcoded_size].into_boxed_slice(); + if let Ok(_) = read_fozdb.read_entry(AUDIOCONV_FOZ_TAG_PTNADATA, hash, 0, &mut buf, fossilize::CRCCheck::WithoutCRC) { + return Ok((buf, 0.0)); + } + } + if try_loop { + if let Ok(transcoded_size) = read_fozdb.entry_size(AUDIOCONV_FOZ_TAG_PTNADATA, loop_hash) { + /* success */ + let mut buf = vec![0u8; transcoded_size].into_boxed_slice(); + if let Ok(_) = read_fozdb.read_entry(AUDIOCONV_FOZ_TAG_PTNADATA, loop_hash, 0, &mut buf, fossilize::CRCCheck::WithoutCRC) { + return Ok((buf, 0.0)); + } + } + } + } + + /* if we can't, return the blank file */ + self.stream_state.needs_dump = true; + + let buf = Box::new(include_bytes!("../blank.ptna").clone()); + + /* calculate average expected length of this buffer */ + let codec_data = self.codec_data.as_ref().unwrap(); + + let mut repeat_count = if codec_data.bitrate > 0 { + let buffer_time_ms = (buf_len * 8 /* to bits */ * 1000 /* to ms */) as f32 + / (codec_data.bitrate as f32); + + /* repeat the known-length blank file enough times to fill the expected length */ + buffer_time_ms / BLANK_AUDIO_FILE_LENGTH_MS + } else { + /* invalid bitrate, so just play the file once */ + 0.0 + }; + + /* scale to output rate */ + repeat_count *= codec_data.rate as f32 / BLANK_AUDIO_FILE_RATE; + + Ok((buf, repeat_count)) + } +} + +struct AudioConv { + state: Mutex>, + sinkpad: gst::Pad, + srcpad: gst::Pad, +} + +impl ObjectSubclass for AudioConv { + const NAME: &'static str = "ProtonAudioConverter"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct; + type Class = subclass::simple::ClassStruct; + + glib_object_subclass!(); + + fn with_class(klass: &subclass::simple::ClassStruct) -> Self { + let templ = klass.get_pad_template("sink").unwrap(); + let sinkpad = gst::Pad::builder_with_template(&templ, Some("sink")) + .chain_function(|pad, parent, buffer| { + AudioConv::catch_panic_pad_function( + parent, + || Err(gst::FlowError::Error), + |audioconv, element| audioconv.chain(pad, element, buffer) + ) + }) + .event_function(|pad, parent, event| { + AudioConv::catch_panic_pad_function( + parent, + || false, + |audioconv, element| audioconv.sink_event(pad, element, event) + ) + }).build(); + + let templ = klass.get_pad_template("src").unwrap(); + let srcpad = gst::Pad::builder_with_template(&templ, Some("src")) + .query_function(|pad, parent, query| { + AudioConv::catch_panic_pad_function( + parent, + || false, + |audioconv, element| audioconv.src_query(pad, element, query) + ) + }) + .activatemode_function(|pad, parent, mode, active| { + AudioConv::catch_panic_pad_function( + parent, + || Err(gst_loggable_error!(CAT, "Panic activating srcpad with mode")), + |audioconv, element| audioconv.src_activatemode(pad, element, mode, active) + ) + }).build(); + + AudioConv { + state: Mutex::new(None), + sinkpad, + srcpad, + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct) { + + klass.set_metadata( + "Proton audio converter", + "Codec/Parser", + "Converts audio for Proton", + "Andrew Eikum "); + + let mut caps = gst::Caps::new_empty(); + { + let caps = caps.get_mut().unwrap(); + caps.append(gst::Caps::builder("audio/x-wma").build()); + } + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps).unwrap(); + klass.add_pad_template(sink_pad_template); + + let caps = gst::Caps::builder("audio/x-opus").build(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps).unwrap(); + klass.add_pad_template(src_pad_template); + } +} + +impl ObjectImpl for AudioConv { + glib_object_impl!(); + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::().unwrap(); + element.add_pad(&self.sinkpad).unwrap(); + element.add_pad(&self.srcpad).unwrap(); + } +} + +impl ElementImpl for AudioConv { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange + ) -> Result { + + gst_log!(CAT, obj: element, "State transition: {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + /* do runtime setup */ + + { + /* open fozdb here; this is the right place to fail and opening may be + * expensive */ + let db = (*DUMP_FOZDB).lock().unwrap(); + if (*db).is_none() { + gst_error!(CAT, "Failed to open fossilize db!"); + return Err(gst::StateChangeError); + } + } + + let new_state = AudioConvState::new().map_err(|err| { + err.log(); + return gst::StateChangeError; + })?; + + let mut state = self.state.lock().unwrap(); + assert!((*state).is_none()); + *state = Some(new_state); + }, + + gst::StateChange::ReadyToNull => { + /* do runtime teardown */ + + let old_state = self.state.lock().unwrap().take(); // dispose of state + if let Some(old_state) = old_state { + if old_state.stream_state.write_to_foz().is_err() { + gst_warning!(CAT, "Error writing out stream data!"); + } + } + }, + + _ => (), + }; + + self.parent_change_state(element, transition) + + /* XXX on ReadyToNull, sodium drops state _again_ here... why? */ + } +} + +impl AudioConv { + + fn chain( + &self, + _pad: &gst::Pad, + _element: &gst::Element, + buffer: gst::Buffer + ) -> Result { + gst_log!(CAT, "Handling buffer {:?}", buffer); + + let mut state = self.state.lock().unwrap(); + let mut state = match &mut *state { + Some(s) => s, + None => { return Err(gst::FlowError::Error); }, + }; + + let (ptnadata, mut repeat_count) = state.open_transcode_file(buffer).map_err(|_| { + gst_error!(CAT, "ERROR! Failed to read transcoded audio! Things will go badly..."); gst::FlowError::Error + })?; + + let mut offs: usize = 0; + loop { + + if offs >= ptnadata.len() { + if repeat_count > 0.0 { + /* TODO: we're sending the whole partial packet. we could set padding instead */ + repeat_count -= 1.0; + offs = 0; + continue; + } else { + break; + } + } + + if offs + 4 >= ptnadata.len() { + gst_warning!(CAT, "Short read on ptna header?"); + break; + } + + let packet_hdr = u32::from_le_bytes(copy_into_array(&ptnadata[offs..offs + 4])); + offs += 4; + + let (flags, padding_len, encoded_len) = + ((packet_hdr & AUDIOCONV_FLAG_MASK), + (packet_hdr & AUDIOCONV_PADDING_LENGTH_MASK) >> AUDIOCONV_PADDING_LENGTH_SHIFT, + (packet_hdr & AUDIOCONV_ENCODED_LENGTH_MASK) as usize); + + if offs + encoded_len > ptnadata.len() { + gst_warning!(CAT, "Short read on ptna data?"); + break; + } + + let pkt_is_header = (flags & AUDIOCONV_FLAG_HEADER) != 0; + + if pkt_is_header && state.sent_header { + /* only send one header */ + offs += encoded_len; + continue; + } + + /* TODO: can we use a gstbuffer cache here? */ + let mut buffer = gst::Buffer::with_size(encoded_len as usize).unwrap(); + + if !pkt_is_header && padding_len > 0 { + gst_audio::AudioClippingMeta::add(buffer.get_mut().unwrap(), gst::format::Default(Some(0)), gst::format::Default(Some(padding_len as u64))); + } + + let mut writable = buffer.into_mapped_buffer_writable().unwrap(); + + writable.as_mut_slice().copy_from_slice(&ptnadata[offs..offs + encoded_len]); + + gst_log!(CAT, "pushing one packet of len {}", encoded_len); + self.srcpad.push(writable.into_buffer())?; + + if pkt_is_header { + state.sent_header = true; + } + + offs += encoded_len; + } + + Ok(gst::FlowSuccess::Ok) + } + + fn sink_event( + &self, + pad: &gst::Pad, + element: &gst::Element, + event: gst::Event + ) -> bool { + gst_log!(CAT, obj:pad, "Got an event {:?}", event); + match event.view() { + EventView::Caps(event_caps) => { + + let mut state = self.state.lock().unwrap(); + if let Some(state) = &mut *state { + let head = match NeedTranscodeHead::new_from_caps(&event_caps.get_caps()){ + Ok(h) => h, + Err(e) => { + gst_error!(CAT, "Invalid WMA caps!"); + e.log(); + return false; + }, + }; + state.codec_data = Some(head); + }; + drop(state); + + let mut caps = gst::Caps::new_empty(); + { + let caps = caps.get_mut().unwrap(); + let s = gst::Structure::builder("audio/x-opus") + .field("channel-mapping-family", &0i32) + .build(); + caps.append_structure(s); + } + + self.srcpad.push_event(gst::event::Caps::new(&caps)) + }, + EventView::FlushStop(_) => { + let mut state = self.state.lock().unwrap(); + if let Some(state) = &mut *state { + state.reset(); + }; + drop(state); + + pad.event_default(Some(element), event) + }, + _ => pad.event_default(Some(element), event) + } + } + + fn src_query( + &self, + pad: &gst::Pad, + element: &gst::Element, + query: &mut gst::QueryRef) -> bool + { + gst_log!(CAT, obj: pad, "got query: {:?}", query); + match query.view_mut() { + QueryView::Scheduling(mut q) => { + let mut peer_query = gst::query::Scheduling::new(); + let res = self.sinkpad.peer_query(&mut peer_query); + if ! res { + return res; + } + + let (flags, min, max, align) = peer_query.get_result(); + + q.set(flags, min, max, align); + true + }, + _ => pad.query_default(Some(element), query) + } + } + + fn src_activatemode( + &self, + _pad: &gst::Pad, + _element: &gst::Element, + mode: gst::PadMode, + active: bool + ) -> Result<(), gst::LoggableError> { + self.sinkpad + .activate_mode(mode, active)?; + + Ok(()) + } + +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "protonaudioconverter", + gst::Rank::Marginal, + AudioConv::get_type()) +} diff --git a/media-converter/src/audioconvbin.rs b/media-converter/src/audioconvbin.rs new file mode 100644 index 00000000..b7aa1851 --- /dev/null +++ b/media-converter/src/audioconvbin.rs @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2020 Valve Corporation + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use glib; +use glib::subclass; +use glib::subclass::prelude::*; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::EventView; + +use once_cell::sync::Lazy; + +/* Opus is a great fit for our usecase except for one problem: it only supports a few samplerates. + * Notably it doesn't support 44100 Hz, which is a very frequently used samplerate. This bin + * provides a capssetter element which will override the rate we get from Opus with the rate the + * application requested. Similarly, on the transcoder side, we just encode the audio as if it were + * at 48 kHz, even if it is actually at 44.1 kHz. + * + * The downside to this is a small decrease in audio quality. If Opus is most responsive between 20 + * Hz and 20 kHz, then when 44.1 audio is converted to 48, we'll gain noise between 18-20 Hz + * (although WMA probably already filtered that out) and start to lose audio above 18,375 kHz. This + * is at the very edge of human hearing, so we're unlikely to lose any noticeable quality. + * + * Resampling is an option, but has some problems. It's significantly more complicated, and more + * CPU-intensive. Also, XAudio2 buffers can be started and ended at arbitrary points, so if we + * start moving audio data from one buffer into another due to resampling, it may result in audible + * artifacts. I think just encoding at the wrong rate is the best compromise. If the application + * actually cared about audio quality, they probably would not have used WMA in the first place. + */ + +static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "protonaudioconverterbin", + gst::DebugColorFlags::empty(), + Some("Proton audio converter bin")) +}); + +struct AudioConvBin { + audioconv: gst::Element, + opusdec: gst::Element, + capssetter: gst::Element, + srcpad: gst::GhostPad, + sinkpad: gst::GhostPad, +} + +impl ObjectSubclass for AudioConvBin { + const NAME: &'static str = "ProtonAudioConverterBin"; + + type ParentType = gst::Bin; + type Instance = gst::subclass::ElementInstanceStruct; + type Class = subclass::simple::ClassStruct; + + glib_object_subclass!(); + + fn with_class(klass: &subclass::simple::ClassStruct) -> Self { + + let templ = klass.get_pad_template("src").unwrap(); + let srcpad = gst::GhostPad::builder_with_template(&templ, Some("src")).build(); + + let templ = klass.get_pad_template("sink").unwrap(); + let sinkpad = gst::GhostPad::builder_with_template(&templ, Some("sink")) + .event_function(|pad, parent, event| { + AudioConvBin::catch_panic_pad_function( + parent, + || false, + |audioconvbin, element| audioconvbin.sink_event(pad, element, event) + ) + }).build(); + + let audioconv = gst::ElementFactory::make("protonaudioconverter", None).unwrap(); + let opusdec = gst::ElementFactory::make("opusdec", None).unwrap(); + let capssetter = gst::ElementFactory::make("capssetter", None).unwrap(); + + AudioConvBin { + audioconv, + opusdec, + capssetter, + srcpad, + sinkpad, + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct) { + klass.set_metadata("Proton audio converter with rate fixup", + "Codec/Parser", + "Converts audio for Proton, fixing up samplerates", + "Andrew Eikum "); + + let mut caps = gst::Caps::new_empty(); + { + let caps = caps.get_mut().unwrap(); + caps.append(gst::Caps::builder("audio/x-wma").build()); + } + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps).unwrap(); + klass.add_pad_template(sink_pad_template); + + let caps = gst::Caps::builder("audio/x-raw").build(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps).unwrap(); + klass.add_pad_template(src_pad_template); + } +} + +impl ObjectImpl for AudioConvBin { + glib_object_impl!(); + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let bin = obj.downcast_ref::().unwrap(); + + bin.add(&self.audioconv).unwrap(); + bin.add(&self.opusdec).unwrap(); + bin.add(&self.capssetter).unwrap(); + + self.audioconv.link(&self.opusdec).unwrap(); + self.opusdec.link(&self.capssetter).unwrap(); + + self.sinkpad + .set_target(Some(&self.audioconv.get_static_pad("sink").unwrap())) + .unwrap(); + self.srcpad + .set_target(Some(&self.capssetter.get_static_pad("src").unwrap())) + .unwrap(); + + bin.add_pad(&self.sinkpad).unwrap(); + bin.add_pad(&self.srcpad).unwrap(); + } +} + +impl BinImpl for AudioConvBin { } + +impl ElementImpl for AudioConvBin { } + +impl AudioConvBin { + fn sink_event( + &self, + pad: &gst::GhostPad, + element: &gst::Element, + event: gst::Event + ) -> bool { + match event.view() { + EventView::Caps(event_caps) => { + /* set up capssetter with this rate */ + if let Some(s) = event_caps.get_caps().get_structure(0) { + + if let Ok(override_rate) = s.get_some::("rate") { + + let mut rate_caps = gst::Caps::new_empty(); + { + let rate_caps = rate_caps.get_mut().unwrap(); + let s = gst::Structure::builder("audio/x-raw") + .field("rate", &override_rate) + .build(); + rate_caps.append_structure(s); + } + self.capssetter.set_property("caps", + &rate_caps).unwrap(); + }else{ + gst_warning!(CAT, "event has no rate"); + } + } else { + gst_warning!(CAT, "event has no structure"); + } + + /* forward on to the real pad */ + self.audioconv.get_static_pad("sink").unwrap() + .send_event(event) + }, + _ => pad.event_default(Some(element), event) + } + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "protonaudioconverterbin", + gst::Rank::Marginal + 1, + AudioConvBin::get_type() + ) +} diff --git a/media-converter/src/fossilize.rs b/media-converter/src/fossilize.rs new file mode 100644 index 00000000..511f4602 --- /dev/null +++ b/media-converter/src/fossilize.rs @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2020 Valve Corporation + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Based on "Fossilize," which is + * Copyright (c) 2018-2019 Hans-Kristian Arntzen + * https://github.com/ValveSoftware/Fossilize/ + */ + +/* This is a read/write implementation of the Fossilize database format. + * + * https://github.com/ValveSoftware/Fossilize/ + * + * That C++ implementation is specific to Vulkan, while this one tries to be generic to store any + * type of data. + * + * FIXME: It should probably live in that repo or in a separate project. + */ + +use std::fs; +use std::io; +use std::io::Read; +use std::io::Write; +use std::io::Seek; +use std::fs::OpenOptions; +use std::convert::From; +use std::collections::HashMap; +use crc32fast; + +use crate::*; + +/* Fossilize StreamArchive database format version 6: + * + * The file consists of a header, followed by an unlimited series of "entries". + * + * All multi-byte entities are little-endian. + * + * The file header is as follows: + * + * Field Type Description + * ----- ---- ----------- + * magic_number uint8_t[12] Constant value: "\x81""FOSSILIZEDB" + * version uint32_t StreamArchive version: 6 + * + * + * Each entry follows this format: + * + * Field Type Description + * ----- ---- ----------- + * name unsigned char[40] Application-defined entry identifier, stored in hexadecimal big-endian + * ASCII. Usually N-char tag followed by (40 - N)-char hash. + * stored_size uint32_t Size of the payload as stored in this file. + * flags uint32_t Flags for this entry (e.g. compression). See below. + * crc32 uint32_t CRC32 of the payload as stored in this file. + * payload_size uint32_t Size of this payload after decompression. + * payload uint8_t[stored_size] Entry data. + * + * The flags field may contain: + * 0x1: No compression. + * 0x2: Deflate compression. + */ + +const FOSSILIZE_MAGIC: [u8; 12] = [0x81, 0x46, 0x4f, 0x53, 0x53, 0x49, 0x4c, 0x49, 0x5a, 0x45, 0x44, 0x42]; +const FOSSILIZE_MIN_COMPAT_VERSION: u8 = 5; +const FOSSILIZE_VERSION: u8 = 6; +const MAGIC_LEN_BYTES: usize = 12 + 4; + +const FOSSILIZE_COMPRESSION_NONE: u32 = 1; +const _FOSSILIZE_COMPRESSION_DEFLATE: u32 = 2; + +#[derive(Debug)] +pub enum Error { + NotImplemented, + IOError(io::Error), + CorruptDatabase, + DataTooLarge, + InvalidTag, + EntryNotFound, + FailedChecksum, +} + +impl From for Error { + fn from(e: io::Error) -> Error { + Error::IOError(e) + } +} + +type FossilizeHash = u128; +const _FOSSILIZEHASH_ASCII_LEN: usize = (128 / 8) * 2; + +trait ToAscii { + fn to_ascii_bytes(&self) -> Vec; + + fn from_ascii_bytes(b: &[u8]) -> Result + where Self: std::marker::Sized; +} + +impl ToAscii for FossilizeHash { + fn to_ascii_bytes(&self) -> Vec { + format_hash(*self).into_bytes() + } + + fn from_ascii_bytes(b: &[u8]) -> Result { + let s = String::from_utf8(b.to_vec()) + .map_err(|_| Error::CorruptDatabase)?; + Self::from_str_radix(&s, 16) + .map_err(|_| Error::CorruptDatabase) + } +} + +type FossilizeTag = u32; +const FOSSILIZETAG_ASCII_LEN: usize = (32 / 8) * 2; + +impl ToAscii for FossilizeTag { + fn to_ascii_bytes(&self) -> Vec { + format!("{:08x}", *self).into_bytes() + } + + fn from_ascii_bytes(b: &[u8]) -> Result { + let s = String::from_utf8(b.to_vec()) + .map_err(|_| Error::CorruptDatabase)?; + Self::from_str_radix(&s, 16) + .map_err(|_| Error::CorruptDatabase) + } +} + +const PAYLOAD_NAME_LEN_BYTES: usize = 40; + +struct PayloadInfo { + size: u32, + compression: u32, + crc: u32, + full_size: u32, +} + +const PAYLOAD_HEADER_LEN_BYTES: usize = 4 * 4; + +impl PayloadInfo { + + fn new_from_slice(dat: &[u8]) -> Self { + Self { + size: u32::from_le_bytes(copy_into_array(&dat[0..4])), + compression: u32::from_le_bytes(copy_into_array(&dat[4..8])), + crc: u32::from_le_bytes(copy_into_array(&dat[8..12])), + full_size: u32::from_le_bytes(copy_into_array(&dat[12..16])), + } + } + + fn to_slice(&self) -> [u8; PAYLOAD_HEADER_LEN_BYTES] { + let mut ret = [0u8; PAYLOAD_HEADER_LEN_BYTES]; + ret[0..4].copy_from_slice(&self.size.to_le_bytes()); + ret[4..8].copy_from_slice(&self.compression.to_le_bytes()); + ret[8..12].copy_from_slice(&self.crc.to_le_bytes()); + ret[12..16].copy_from_slice(&self.full_size.to_le_bytes()); + ret + } +} + +pub struct PayloadEntry { + offset: u64, + payload_info: PayloadInfo, +} + +impl PayloadEntry { + fn new_from_slice(offset: u64, dat: &[u8]) -> Self { + Self { + offset, + payload_info: PayloadInfo::new_from_slice(dat), + } + } +} + +pub struct StreamArchive { + file: fs::File, + + seen_blobs: Vec>, + + write_pos: u64, +} + +pub enum CRCCheck { + WithoutCRC, + WithCRC, +} + +impl StreamArchive { + + pub fn new>(filename: P, fileopts: &OpenOptions, num_tags: usize) -> Result { + + let file = fileopts.open(filename)?; + + let mut seen_blobs = Vec::new(); + for _ in 0..num_tags { + seen_blobs.push(HashMap::new()); + } + + let mut ret = Self { + file, + seen_blobs, + write_pos: 0, + }; + + ret.prepare()?; + + Ok(ret) + } + + pub fn prepare(&mut self) -> Result<(), Error> { + self.write_pos = self.file.seek(io::SeekFrom::Start(0))?; + + if self.file.metadata().unwrap().len() > 0 { + let mut magic_and_version = [0 as u8; MAGIC_LEN_BYTES]; + self.file.read_exact(&mut magic_and_version)?; + + let version = magic_and_version[15]; + + if magic_and_version[0..12] != FOSSILIZE_MAGIC || + version < FOSSILIZE_MIN_COMPAT_VERSION || + version > FOSSILIZE_VERSION { + return Err(Error::CorruptDatabase); + } + loop { + let mut name_and_header = [0u8; PAYLOAD_NAME_LEN_BYTES + PAYLOAD_HEADER_LEN_BYTES]; + let res = self.file.read_exact(&mut name_and_header); + + if let Err(fail) = res { + if fail.kind() == io::ErrorKind::UnexpectedEof { + break; + } + return Err(Error::IOError(fail)); + } + + let name = &name_and_header[0..PAYLOAD_NAME_LEN_BYTES]; + + let tag: usize = FossilizeTag::from_ascii_bytes(&name[0..FOSSILIZETAG_ASCII_LEN])? as usize; + let hash = FossilizeHash::from_ascii_bytes(&name[FOSSILIZETAG_ASCII_LEN..])?; + + let payload_entry = PayloadEntry::new_from_slice( + self.file.seek(io::SeekFrom::Current(0))?, + &name_and_header[PAYLOAD_NAME_LEN_BYTES..] + ); + + let res = self.file.seek(io::SeekFrom::Current(payload_entry.payload_info.size as i64)); + match res { + Ok(p) => { + self.write_pos = p; + self.seen_blobs[tag].insert(hash, payload_entry); + }, + + Err(e) => { + /* truncated chunk is not fatal */ + if e.kind() != io::ErrorKind::UnexpectedEof { + return Err(Error::IOError(e)); + } + }, + } + } + } else { + /* new file, write foz header */ + self.file.write_all(&FOSSILIZE_MAGIC)?; + self.file.write_all(&[0u8, 0u8, 0u8, FOSSILIZE_VERSION])?; + + self.write_pos = MAGIC_LEN_BYTES as u64; + } + + Ok(()) + } + + pub fn has_entry(&self, tag: FossilizeTag, hash: FossilizeHash) -> bool { + self.seen_blobs[tag as usize].contains_key(&hash) + } + + pub fn iter_tag(&self, tag: FossilizeTag) -> std::collections::hash_map::Keys { + self.seen_blobs[tag as usize].keys() + } + + pub fn entry_size(&self, tag: FossilizeTag, hash: FossilizeHash) -> Result { + match self.seen_blobs[tag as usize].get(&hash) { + None => Err(Error::EntryNotFound), + Some(e) => Ok(e.payload_info.full_size as usize), + } + } + + pub fn read_entry(&mut self, tag: FossilizeTag, hash: FossilizeHash, offset: u64, buf: &mut [u8], crc_opt: CRCCheck) -> Result { + if tag as usize >= self.seen_blobs.len() { + return Err(Error::InvalidTag); + } + + let entry = &self.seen_blobs[tag as usize].get(&hash); + + let entry = match entry { + None => { return Err(Error::EntryNotFound); } + Some(e) => e, + }; + + if entry.payload_info.compression != FOSSILIZE_COMPRESSION_NONE { + return Err(Error::NotImplemented); + } + + if offset >= entry.payload_info.full_size as u64 { + return Ok(0); + } + + self.file.seek(io::SeekFrom::Start(entry.offset + offset))?; + + let to_copy = std::cmp::min(entry.payload_info.full_size as usize - offset as usize, buf.len()); + + self.file.read_exact(&mut buf[0..to_copy]) + .map_err(|e| Error::IOError(e))?; + + if entry.payload_info.crc != 0 { + if let CRCCheck::WithCRC = crc_opt { + let mut crc_hasher = crc32fast::Hasher::new(); + crc_hasher.update(&buf[0..to_copy]); + let got_crc = crc_hasher.finalize(); + if got_crc != entry.payload_info.crc { + return Err(Error::FailedChecksum); + } + } + } + + Ok(to_copy) + } + + pub fn write_entry(&mut self, tag: FossilizeTag, hash: FossilizeHash, data: &mut dyn Read, crc_opt: CRCCheck) -> Result<(), Error> { + if self.has_entry(tag, hash) { + return Ok(()); + } + + self.file.seek(io::SeekFrom::Start(self.write_pos))?; + + /* write entry name */ + let mut name = [0u8; PAYLOAD_NAME_LEN_BYTES]; + + name[0..FOSSILIZETAG_ASCII_LEN].copy_from_slice(&tag.to_ascii_bytes()); + + name[FOSSILIZETAG_ASCII_LEN..].copy_from_slice(&hash.to_ascii_bytes()); + + self.file.write_all(&name)?; + + /* allocate payload info space */ + let payload_info_pos = self.file.seek(io::SeekFrom::Current(0))?; + + let payload_info = PayloadInfo { + size: u32::max_value(), /* will be filled later */ + compression: FOSSILIZE_COMPRESSION_NONE, + crc: 0, /* will be filled later */ + full_size: u32::max_value(), /* will be filled later */ + }; + + self.file.write_all(&payload_info.to_slice())?; + + /* write data */ + let mut payload_entry = PayloadEntry { + offset: self.file.seek(io::SeekFrom::Current(0))?, + payload_info, + }; + + const BUFFER_COPY_BYTES: usize = 8 * 1024 * 1024; /* tuneable */ + let mut buf = box_array![0u8; BUFFER_COPY_BYTES]; + let mut size: usize = 0; + let mut crc_hasher = crc32fast::Hasher::new(); + loop { + let readed = data.read(&mut *buf)?; + if readed == 0 { + break; + } + + if size + readed > u32::max_value() as usize { + /* Fossilize format only supports 4 GiB entries */ + return Err(Error::DataTooLarge); + } + + size += readed; + + self.file.write_all(&buf[0..readed])?; + + if let CRCCheck::WithCRC = crc_opt { + crc_hasher.update(&buf[0..readed]); + } + } + + self.write_pos = self.file.seek(io::SeekFrom::Current(0))?; + + /* seek back and fill in the size */ + self.file.seek(io::SeekFrom::Start(payload_info_pos))?; + + payload_entry.payload_info.size = size as u32; + payload_entry.payload_info.full_size = size as u32; + + if let CRCCheck::WithCRC = crc_opt { + payload_entry.payload_info.crc = crc_hasher.finalize(); + } + + self.file.write_all(&payload_entry.payload_info.to_slice())?; + + /* success. record entry and exit */ + self.seen_blobs[tag as usize].insert(hash, payload_entry); + + Ok(()) + } +} diff --git a/media-converter/src/lib.rs b/media-converter/src/lib.rs new file mode 100644 index 00000000..e06eb587 --- /dev/null +++ b/media-converter/src/lib.rs @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2020 Valve Corporation + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#[macro_use] +extern crate glib; +#[macro_use] +extern crate gstreamer as gst; +extern crate gstreamer_base as gst_base; +extern crate gstreamer_video as gst_video; +extern crate gstreamer_audio as gst_audio; +extern crate once_cell; + +use std::io; +use std::io::Read; + +#[cfg(target_arch = "x86")] +mod murmur3_x86_128; +#[cfg(target_arch = "x86_64")] +mod murmur3_x64_128; + +mod videoconv; +mod audioconv; +mod audioconvbin; +mod fossilize; + +// copy_into_array: +// +// Copyright (c) 2020 Stu Small +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +fn copy_into_array(slice: &[T]) -> A +where + A: Default + AsMut<[T]>, + T: Copy, +{ + let mut a = A::default(); + >::as_mut(&mut a).copy_from_slice(slice); + a +} + +/* rust has a hard time with large heap allocations. below macro works around that. + * + * by @simias from https://github.com/rust-lang/rust/issues/53827 */ +#[macro_export] +macro_rules! box_array { + ($val:expr ; $len:expr) => {{ + // Use a generic function so that the pointer cast remains type-safe + fn vec_to_boxed_array(vec: Vec) -> Box<[T; $len]> { + let boxed_slice = vec.into_boxed_slice(); + + let ptr = ::std::boxed::Box::into_raw(boxed_slice) as *mut [T; $len]; + + unsafe { Box::from_raw(ptr) } + } + + vec_to_boxed_array(vec![$val; $len]) + }}; +} + +/* you MUST use this to consistently format the hash bytes into a string */ +fn format_hash(hash: u128) -> String { + return format!("{:032x}", hash); +} + +/* changing this will invalidate the cache. you MUST clear it. */ +const HASH_SEED: u32 = 0x4AA61F63; + +pub struct BufferedReader<'a> { + dat: &'a [u8], + len: usize, + ofs: usize, +} + +impl<'a> BufferedReader<'a> { + fn new(dat: &'a [u8], len: usize) -> Self { + BufferedReader { + dat, + len, + ofs: 0, + } + } +} + +impl<'a> Read for BufferedReader<'a> { + fn read(&mut self, out: &mut [u8]) -> io::Result { + let to_copy = std::cmp::min(self.len - self.ofs, out.len()); + + if to_copy == 0 { + return Ok(0); + } + + if to_copy == out.len() { + out.copy_from_slice(&self.dat[self.ofs..(self.ofs + to_copy)]); + }else{ + out[0..to_copy].copy_from_slice(&self.dat[self.ofs..(self.ofs + to_copy)]); + } + + self.ofs += to_copy; + + Ok(to_copy) + } +} + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + videoconv::register(plugin)?; + audioconvbin::register(plugin)?; + audioconv::register(plugin)?; + Ok(()) +} + +gst_plugin_define!( + protonmediaconverter, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "MIT/X11", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); diff --git a/media-converter/src/murmur3_x64_128.rs b/media-converter/src/murmur3_x64_128.rs new file mode 100644 index 00000000..c1846385 --- /dev/null +++ b/media-converter/src/murmur3_x64_128.rs @@ -0,0 +1,232 @@ +// Copyright (c) 2020 Stu Small +// +// Modified to return its internal state for continuous hashing: +// Copyright (c) 2020 Andrew Eikum for CodeWeavers +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use std::io::{Read, Result}; +use std::ops::Shl; + +use crate::copy_into_array; + +#[allow(non_camel_case_types)] +pub struct murmur3_x64_128_state { + seed: u32, + h1: u64, + h2: u64, + processed: usize, +} + +impl murmur3_x64_128_state { + pub fn new(seed: u32) -> Self { + murmur3_x64_128_state { + seed, + h1: seed as u64, + h2: seed as u64, + processed: 0, + } + } + + #[allow(dead_code)] + pub fn reset(&mut self) { + self.h1 = self.seed as u64; + self.h2 = self.seed as u64; + self.processed = 0; + } +} + +/// Use the x64 variant of the 128 bit murmur3 to hash some [Read] implementation. +/// +/// # Example +/// ``` +/// use std::io::Cursor; +/// use murmur3::murmur3_x64_128; +/// let hash_result = murmur3_x64_128(&mut Cursor::new("hello world"), 0); +/// ``` +pub fn murmur3_x64_128(source: &mut T, seed: u32) -> Result { + let mut state = murmur3_x64_128_state::new(seed); + murmur3_x64_128_full(source, &mut state) +} + +pub fn murmur3_x64_128_full(source: &mut T, state: &mut murmur3_x64_128_state) -> Result { + const C1: u64 = 0x87c3_7b91_1142_53d5; + const C2: u64 = 0x4cf5_ad43_2745_937f; + const C3: u64 = 0x52dc_e729; + const C4: u64 = 0x3849_5ab5; + const R1: u32 = 27; + const R2: u32 = 31; + const R3: u32 = 33; + const M: u64 = 5; + let mut h1: u64 = state.h1; + let mut h2: u64 = state.h2; + let mut buf = [0; 16]; + let mut processed: usize = state.processed; + loop { + let read = source.read(&mut buf[..])?; + processed += read; + if read == 16 { + let k1 = u64::from_le_bytes(copy_into_array(&buf[0..8])); + let k2 = u64::from_le_bytes(copy_into_array(&buf[8..])); + h1 ^= k1.wrapping_mul(C1).rotate_left(R2).wrapping_mul(C2); + h1 = h1 + .rotate_left(R1) + .wrapping_add(h2) + .wrapping_mul(M) + .wrapping_add(C3); + h2 ^= k2.wrapping_mul(C2).rotate_left(R3).wrapping_mul(C1); + h2 = h2 + .rotate_left(R2) + .wrapping_add(h1) + .wrapping_mul(M) + .wrapping_add(C4); + } else if read == 0 { + state.h1 = h1; + state.h2 = h2; + state.processed = processed; + h1 ^= processed as u64; + h2 ^= processed as u64; + h1 = h1.wrapping_add(h2); + h2 = h2.wrapping_add(h1); + h1 = fmix64(h1); + h2 = fmix64(h2); + h1 = h1.wrapping_add(h2); + h2 = h2.wrapping_add(h1); + let x = ((h2 as u128) << 64) | (h1 as u128); + return Ok(x); + } else { + let mut k1 = 0; + let mut k2 = 0; + if read >= 15 { + k2 ^= (buf[14] as u64).shl(48); + } + if read >= 14 { + k2 ^= (buf[13] as u64).shl(40); + } + if read >= 13 { + k2 ^= (buf[12] as u64).shl(32); + } + if read >= 12 { + k2 ^= (buf[11] as u64).shl(24); + } + if read >= 11 { + k2 ^= (buf[10] as u64).shl(16); + } + if read >= 10 { + k2 ^= (buf[9] as u64).shl(8); + } + if read >= 9 { + k2 ^= buf[8] as u64; + k2 = k2.wrapping_mul(C2).rotate_left(33).wrapping_mul(C1); + h2 ^= k2; + } + if read >= 8 { + k1 ^= (buf[7] as u64).shl(56); + } + if read >= 7 { + k1 ^= (buf[6] as u64).shl(48); + } + if read >= 6 { + k1 ^= (buf[5] as u64).shl(40); + } + if read >= 5 { + k1 ^= (buf[4] as u64).shl(32); + } + if read >= 4 { + k1 ^= (buf[3] as u64).shl(24); + } + if read >= 3 { + k1 ^= (buf[2] as u64).shl(16); + } + if read >= 2 { + k1 ^= (buf[1] as u64).shl(8); + } + if read >= 1 { + k1 ^= buf[0] as u64; + } + k1 = k1.wrapping_mul(C1); + k1 = k1.rotate_left(31); + k1 = k1.wrapping_mul(C2); + h1 ^= k1; + } + } +} + +fn fmix64(k: u64) -> u64 { + const C1: u64 = 0xff51_afd7_ed55_8ccd; + const C2: u64 = 0xc4ce_b9fe_1a85_ec53; + const R: u32 = 33; + let mut tmp = k; + tmp ^= tmp >> R; + tmp = tmp.wrapping_mul(C1); + tmp ^= tmp >> R; + tmp = tmp.wrapping_mul(C2); + tmp ^= tmp >> R; + tmp +} + +#[cfg(test)] +mod tests { + use super::*; + use std::cmp::min; + use std::io; + use std::io::Read; + + const TEST_SEED: u32 = 0x00000000; + + const CONST_DATA: [u8; 64] = + [ 0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, + 10u8, 11u8, 12u8, 13u8, 14u8, 15u8, 16u8, 17u8, + 20u8, 21u8, 22u8, 23u8, 24u8, 25u8, 26u8, 27u8, + 30u8, 31u8, 32u8, 33u8, 34u8, 35u8, 36u8, 37u8, + 40u8, 41u8, 42u8, 43u8, 44u8, 45u8, 46u8, 47u8, + 50u8, 51u8, 52u8, 53u8, 54u8, 55u8, 56u8, 57u8, + 60u8, 61u8, 62u8, 63u8, 64u8, 65u8, 66u8, 67u8, + 70u8, 71u8, 72u8, 73u8, 74u8, 75u8, 76u8, 77u8 ]; + + struct TestReader<'a> { + data: &'a [u8], + ofs: usize, + } + + impl<'a> TestReader<'a> { + fn new(data: &'a [u8]) -> Self { + TestReader { + data, + ofs: 0, + } + } + } + + impl<'a> Read for TestReader<'a> { + fn read(&mut self, out: &mut [u8]) -> io::Result { + let to_copy = min(out.len(), self.data.len() - self.ofs); + + if to_copy > 0 { + out[0..to_copy].copy_from_slice(&self.data[self.ofs..(self.ofs + to_copy)]); + self.ofs += to_copy; + } + + Ok(to_copy) + } + } + + #[test] + fn test_full_hash() { + /* test with the full buffer */ + let full_hash = murmur3_x64_128(&mut TestReader::new(&CONST_DATA), TEST_SEED).unwrap(); + assert_eq!(full_hash, 0xeb91a9599de8337d969b1e101c4ee3bc); + + /* accumulate hash across 16-byte chunks (short reads change hash due to 0-padding) */ + let mut hash_state = murmur3_x64_128_state::new(TEST_SEED); + murmur3_x64_128_full(&mut TestReader::new(&CONST_DATA[0..16]), &mut hash_state).unwrap(); + murmur3_x64_128_full(&mut TestReader::new(&CONST_DATA[16..32]), &mut hash_state).unwrap(); + murmur3_x64_128_full(&mut TestReader::new(&CONST_DATA[32..48]), &mut hash_state).unwrap(); + let hash = murmur3_x64_128_full(&mut TestReader::new(&CONST_DATA[48..64]), &mut hash_state).unwrap(); + assert_eq!(hash, full_hash); + } +} diff --git a/media-converter/src/murmur3_x86_128.rs b/media-converter/src/murmur3_x86_128.rs new file mode 100644 index 00000000..d20ae158 --- /dev/null +++ b/media-converter/src/murmur3_x86_128.rs @@ -0,0 +1,216 @@ +// Copyright (c) 2020 Stu Small +// +// Modified to return its internal state for continuous hashing: +// Copyright (c) 2020 Andrew Eikum for CodeWeavers +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use std::io::{Read, Result}; +use std::ops::Shl; + +use crate::copy_into_array; + +#[allow(non_camel_case_types)] +pub struct murmur3_x86_128_state { + seed: u32, + h1: u32, + h2: u32, + h3: u32, + h4: u32, + processed: usize, +} + +impl murmur3_x86_128_state { + pub fn new(seed: u32) -> Self { + murmur3_x86_128_state { + seed, + h1: seed, + h2: seed, + h3: seed, + h4: seed, + processed: 0, + } + } + + #[allow(dead_code)] + pub fn reset(&mut self) { + self.h1 = self.seed; + self.h2 = self.seed; + self.h3 = self.seed; + self.h4 = self.seed; + self.processed = 0; + } +} + +/// Use the x86 variant of the 128 bit murmur3 to hash some [Read] implementation. +/// +/// # Example +/// ``` +/// use std::io::Cursor; +/// use murmur3::murmur3_x86_128; +/// let hash_result = murmur3_x86_128(&mut Cursor::new("hello world"), 0); +/// ``` +pub fn murmur3_x86_128(source: &mut T, seed: u32) -> Result { + let mut state = murmur3_x86_128_state::new(seed); + murmur3_x86_128_full(source, &mut state) +} + +pub fn murmur3_x86_128_full(source: &mut T, state: &mut murmur3_x86_128_state) -> Result { + const C1: u32 = 0x239b_961b; + const C2: u32 = 0xab0e_9789; + const C3: u32 = 0x38b3_4ae5; + const C4: u32 = 0xa1e3_8b93; + const C5: u32 = 0x561c_cd1b; + const C6: u32 = 0x0bca_a747; + const C7: u32 = 0x96cd_1c35; + const C8: u32 = 0x32ac_3b17; + const M: u32 = 5; + + let mut h1: u32 = state.h1; + let mut h2: u32 = state.h2; + let mut h3: u32 = state.h3; + let mut h4: u32 = state.h4; + + let mut buf = [0; 16]; + let mut processed: usize = state.processed; + loop { + let read = source.read(&mut buf[..])?; + processed += read; + if read == 16 { + let k1 = u32::from_le_bytes(copy_into_array(&buf[0..4])); + let k2 = u32::from_le_bytes(copy_into_array(&buf[4..8])); + let k3 = u32::from_le_bytes(copy_into_array(&buf[8..12])); + let k4 = u32::from_le_bytes(copy_into_array(&buf[12..16])); + h1 ^= k1.wrapping_mul(C1).rotate_left(15).wrapping_mul(C2); + h1 = h1 + .rotate_left(19) + .wrapping_add(h2) + .wrapping_mul(M) + .wrapping_add(C5); + h2 ^= k2.wrapping_mul(C2).rotate_left(16).wrapping_mul(C3); + h2 = h2 + .rotate_left(17) + .wrapping_add(h3) + .wrapping_mul(M) + .wrapping_add(C6); + h3 ^= k3.wrapping_mul(C3).rotate_left(17).wrapping_mul(C4); + h3 = h3 + .rotate_left(15) + .wrapping_add(h4) + .wrapping_mul(M) + .wrapping_add(C7); + h4 ^= k4.wrapping_mul(C4).rotate_left(18).wrapping_mul(C1); + h4 = h4 + .rotate_left(13) + .wrapping_add(h1) + .wrapping_mul(M) + .wrapping_add(C8); + } else if read == 0 { + state.h1 = h1; + state.h2 = h2; + state.h3 = h3; + state.h4 = h4; + state.processed = processed; + h1 ^= processed as u32; + h2 ^= processed as u32; + h3 ^= processed as u32; + h4 ^= processed as u32; + h1 = h1.wrapping_add(h2); + h1 = h1.wrapping_add(h3); + h1 = h1.wrapping_add(h4); + h2 = h2.wrapping_add(h1); + h3 = h3.wrapping_add(h1); + h4 = h4.wrapping_add(h1); + h1 = fmix32(h1); + h2 = fmix32(h2); + h3 = fmix32(h3); + h4 = fmix32(h4); + h1 = h1.wrapping_add(h2); + h1 = h1.wrapping_add(h3); + h1 = h1.wrapping_add(h4); + h2 = h2.wrapping_add(h1); + h3 = h3.wrapping_add(h1); + h4 = h4.wrapping_add(h1); + let x = ((h4 as u128) << 96) | ((h3 as u128) << 64) | ((h2 as u128) << 32) | h1 as u128; + return Ok(x); + } else { + let mut k1 = 0; + let mut k2 = 0; + let mut k3 = 0; + let mut k4 = 0; + if read >= 15 { + k4 ^= (buf[14] as u32).shl(16); + } + if read >= 14 { + k4 ^= (buf[13] as u32).shl(8); + } + if read >= 13 { + k4 ^= buf[12] as u32; + k4 = k4.wrapping_mul(C4).rotate_left(18).wrapping_mul(C1); + h4 ^= k4; + } + if read >= 12 { + k3 ^= (buf[11] as u32).shl(24); + } + if read >= 11 { + k3 ^= (buf[10] as u32).shl(16); + } + if read >= 10 { + k3 ^= (buf[9] as u32).shl(8); + } + if read >= 9 { + k3 ^= buf[8] as u32; + k3 = k3.wrapping_mul(C3).rotate_left(17).wrapping_mul(C4); + h3 ^= k3; + } + if read >= 8 { + k2 ^= (buf[7] as u32).shl(24); + } + if read >= 7 { + k2 ^= (buf[6] as u32).shl(16); + } + if read >= 6 { + k2 ^= (buf[5] as u32).shl(8); + } + if read >= 5 { + k2 ^= buf[4] as u32; + k2 = k2.wrapping_mul(C2).rotate_left(16).wrapping_mul(C3); + h2 ^= k2; + } + if read >= 4 { + k1 ^= (buf[3] as u32).shl(24); + } + if read >= 3 { + k1 ^= (buf[2] as u32).shl(16); + } + if read >= 2 { + k1 ^= (buf[1] as u32).shl(8); + } + if read >= 1 { + k1 ^= buf[0] as u32; + } + k1 = k1.wrapping_mul(C1); + k1 = k1.rotate_left(15); + k1 = k1.wrapping_mul(C2); + h1 ^= k1; + } + } +} + +fn fmix32(k: u32) -> u32 { + const C1: u32 = 0x85eb_ca6b; + const C2: u32 = 0xc2b2_ae35; + const R1: u32 = 16; + const R2: u32 = 13; + let mut tmp = k; + tmp ^= tmp >> R1; + tmp = tmp.wrapping_mul(C1); + tmp ^= tmp >> R2; + tmp = tmp.wrapping_mul(C2); + tmp ^= tmp >> R1; + tmp +} diff --git a/media-converter/src/videoconv.rs b/media-converter/src/videoconv.rs new file mode 100644 index 00000000..ad0137f6 --- /dev/null +++ b/media-converter/src/videoconv.rs @@ -0,0 +1,709 @@ +/* + * Copyright (c) 2020 Valve Corporation + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use glib; +use glib::subclass; +use glib::subclass::prelude::*; + +use crate::format_hash; +use crate::HASH_SEED; +use crate::box_array; +use crate::BufferedReader; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::EventView; +use gst::QueryView; + +use std::sync::Mutex; +use std::fs; +use std::io; +use std::io::Read; +use std::fs::OpenOptions; + +#[cfg(target_arch = "x86")] +use crate::murmur3_x86_128::murmur3_x86_128 as murmur3_128; +#[cfg(target_arch = "x86_64")] +use crate::murmur3_x64_128::murmur3_x64_128 as murmur3_128; + +use crate::fossilize; + +use once_cell::sync::Lazy; + +/* Algorithm + * --------- + * + * Nicely, both Quartz and Media Foundation allow us random access to the entire data stream. So we + * can easily hash the entire incoming stream and substitute it with our Ogg Theora video. If there + * is a cache miss, then we dump the entire incoming stream. In case of a cache hit, we dump + * nothing. + * + * Incoming video data is stored in the video.foz Fossilize database. + * + * Transcoded video data is stored in the transcoded_video.foz Fossilize database. + * + * + * Hashing algorithm + * ----------------- + * + * We use murmur3 hash with the seed given below. We use the x32 variant for 32-bit programs, and + * the x64 variant for 64-bit programs. + * + * For speed when hashing, we specify a stride which will skip over chunks of the input. However, + * we will always hash the first "stride" number of bytes, to try to avoid collisions on smaller + * files with size between chunk and stride. + * + * For example, the 'H's below are hashed, the 'x's are skipped: + * + * let chunk = 4; + * let stride = chunk * 3; + * H = hashed, x = skipped + * [HHHH HHHH HHHH HHHH xxxx xxxx HHHH xxxx xxxx HHHH xxxx] < data stream + * ^^^^ ^^^^ ^^^^ stride prefix, hashed + * ^^^^ chunk + * ^^^^ ^^^^ ^^^^ stride + * ^^^^ chunk + * ^^^^ ^^^^ ^^^^ stride + * ^^^^ chunk + * ^^^^ ^^^^ stride + */ + +/* changing any of these will invalidate the cache. you MUST clear it. */ +const HASH_CHUNK_SIZE: usize = 8 * 1024 /* to kiB */ * 1024 /* to MiB */; +const HASH_STRIDE: usize = 6 * HASH_CHUNK_SIZE; + +static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "protonvideoconverter", + gst::DebugColorFlags::empty(), + Some("Proton video converter")) +}); + +const VIDEOCONV_FOZ_TAG_VIDEODATA: u32 = 0; +const VIDEOCONV_FOZ_TAG_OGVDATA: u32 = 1; +const VIDEOCONV_FOZ_TAG_STREAM: u32 = 2; +const VIDEOCONV_FOZ_NUM_TAGS: usize = 3; + +static DUMP_FOZDB: Lazy>> = Lazy::new(|| { + let dump_file_path = match std::env::var("MEDIACONV_VIDEO_DUMP_FILE") { + Err(_) => { return Mutex::new(None); }, + Ok(c) => c, + }; + + let dump_file_path = std::path::Path::new(&dump_file_path); + + if fs::create_dir_all(&dump_file_path.parent().unwrap()).is_err() { + return Mutex::new(None); + } + + match fossilize::StreamArchive::new(&dump_file_path, OpenOptions::new().write(true).read(true).create(true), VIDEOCONV_FOZ_NUM_TAGS) { + Ok(newdb) => Mutex::new(Some(newdb)), + Err(_) => Mutex::new(None), + } +}); + +struct PadReader<'a> { + pad: &'a gst::Pad, + offs: usize, + chunk: Box<[u8; HASH_CHUNK_SIZE]>, + chunk_offs: usize, + chunk_end: usize, + stride: usize, /* set to usize::max_value() to skip no bytes */ +} + +impl<'a> PadReader<'a> { + + fn new_with_stride(pad: &'a gst::Pad, stride: usize) -> Self { + PadReader { + pad, + offs: 0, + chunk: box_array![0u8; HASH_CHUNK_SIZE], + chunk_offs: 0, + chunk_end: 0, + stride + } + } + + fn new(pad: &'a gst::Pad) -> Self { + Self::new_with_stride(pad, usize::max_value()) + } +} + +impl<'a> Read for PadReader<'a> { + fn read(&mut self, out: &mut [u8]) -> io::Result { + if self.chunk_offs >= self.chunk_end { + self.chunk_offs = 0; + self.chunk_end = 0; + + let buf = self.pad.pull_range(self.offs as u64, HASH_CHUNK_SIZE as u32); + + match buf { + Err(err) => { + /* on Eos, keep going; we'll return later */ + if err != gst::FlowError::Eos { + return Err(io::Error::new(io::ErrorKind::Other, "upstream pull_range failed" /* TODO can we print our gst err here? */)); + } + }, + Ok(buf) => { + let to_copy; + + if self.offs + buf.get_size() < self.stride { + to_copy = buf.get_size(); + self.offs += to_copy; + }else if self.offs < self.stride { + to_copy = self.stride - self.offs; + self.offs = self.stride; + }else{ + to_copy = buf.get_size(); + self.offs += self.stride; + }; + + if out.len() >= to_copy { + /* copy directly into out buffer and return */ + return Ok( + match buf.copy_to_slice(0, &mut out[0..to_copy]) { + Err(c) => c, + Ok(_) => to_copy, + }); + } else { + self.chunk_end = match buf.copy_to_slice(0, &mut (*self.chunk)[0..to_copy]) { + Err(c) => c, + Ok(_) => to_copy, + }; + } + } + } + } + + if self.chunk_offs >= self.chunk_end { + return Ok(0); + } + + let to_copy = std::cmp::min(self.chunk_end - self.chunk_offs, out.len()); + + if to_copy == 0 { + return Ok(0); + } + + if to_copy == out.len() { + out.copy_from_slice(&self.chunk[self.chunk_offs..(self.chunk_offs + to_copy)]); + }else{ + /* FIXME: there's probably some cool functional way to do this */ + for i in 0..to_copy { + out[i] = self.chunk[self.chunk_offs + i]; + } + } + + self.chunk_offs += to_copy; + + Ok(to_copy) + } +} + +struct VideoConvState { + transcode_hash: Option, + + read_fozdb: Option, + + upstream_duration: Option, + our_duration: Option, +} + +impl VideoConvState { + fn new() -> Result { + + let read_fozdb_path = std::env::var("MEDIACONV_VIDEO_TRANSCODED_FILE").map_err(|_| { + gst_loggable_error!(CAT, "MEDIACONV_VIDEO_TRANSCODED_FILE is not set!") + })?; + + let read_fozdb = match fossilize::StreamArchive::new(&read_fozdb_path, OpenOptions::new().read(true), VIDEOCONV_FOZ_NUM_TAGS) { + Ok(s) => Some(s), + Err(_) => None, + }; + + Ok(VideoConvState { + transcode_hash: None, + + read_fozdb, + + upstream_duration: None, + our_duration: None, + }) + } + + /* true if the file is transcoded; false if not */ + fn begin_transcode(&mut self, hash: u128) -> bool { + if let Some(read_fozdb) = &mut self.read_fozdb { + if let Ok(transcoded_size) = read_fozdb.entry_size(VIDEOCONV_FOZ_TAG_OGVDATA, hash) { + self.transcode_hash = Some(hash); + self.our_duration = Some(transcoded_size as u64); + return true; + } + } + + gst_log!(CAT, "No transcoded video for {}. Substituting a blank video.", format_hash(hash)); + + self.transcode_hash = None; + self.our_duration = Some(include_bytes!("../blank.ogv").len() as u64); + + false + } + + fn fill_buffer(&mut self, offs: usize, out: &mut [u8]) -> Result { + match self.transcode_hash { + Some(hash) => { + let read_fozdb = self.read_fozdb.as_mut().unwrap(); + read_fozdb.read_entry(VIDEOCONV_FOZ_TAG_OGVDATA, hash, offs as u64, out, fossilize::CRCCheck::WithoutCRC) + .map_err(|e| gst_loggable_error!(CAT, "Error reading ogvdata: {:?}", e)) + }, + + None => { + let blank = include_bytes!("../blank.ogv"); + + let remaining = blank.len() - offs; + + if out.len() <= remaining { + out.copy_from_slice(&blank[offs..(offs + out.len())]); + Ok(out.len()) + }else{ + /* FIXME: there's probably some cool functional way to do this */ + for i in 0..remaining { + out[i] = blank[offs + i]; + } + Ok(remaining) + } + }, + } + } +} + +struct VideoConv { + state: Mutex>, + sinkpad: gst::Pad, + srcpad: gst::Pad, +} + +impl ObjectSubclass for VideoConv { + const NAME: &'static str = "ProtonVideoConverter"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct; + type Class = subclass::simple::ClassStruct; + + glib_object_subclass!(); + + fn with_class(klass: &subclass::simple::ClassStruct) -> Self { + let templ = klass.get_pad_template("sink").unwrap(); + let sinkpad = gst::Pad::builder_with_template(&templ, Some("sink")) + .event_function(|pad, parent, event| { + VideoConv::catch_panic_pad_function( + parent, + || false, + |videoconv, element| videoconv.sink_event(pad, element, event) + ) + }).build(); + + let templ = klass.get_pad_template("src").unwrap(); + let srcpad = gst::Pad::builder_with_template(&templ, Some("src")) + .getrange_function(|pad, parent, offset, in_buf, size| { + VideoConv::catch_panic_pad_function( + parent, + || Err(gst::FlowError::Error), + |videoconv, element| videoconv.get_range(pad, element, offset, in_buf, size) + ) + }) + .query_function(|pad, parent, query| { + VideoConv::catch_panic_pad_function( + parent, + || false, + |videoconv, element| videoconv.src_query(pad, element, query) + ) + }) + .activatemode_function(|pad, parent, mode, active| { + VideoConv::catch_panic_pad_function( + parent, + || Err(gst_loggable_error!(CAT, "Panic activating srcpad with mode")), + |videoconv, element| videoconv.src_activatemode(pad, element, mode, active) + ) + }).build(); + + VideoConv { + state: Mutex::new(None), + sinkpad, + srcpad, + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct) { + + klass.set_metadata( + "Proton video converter", + "Codec/Parser", + "Converts video for Proton", + "Andrew Eikum "); + + let mut caps = gst::Caps::new_empty(); + { + let caps = caps.get_mut().unwrap(); + caps.append(gst::Caps::builder("video/x-ms-asf").build()); + caps.append(gst::Caps::builder("video/x-msvideo").build()); + caps.append(gst::Caps::builder("video/mpeg").build()); + } + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps).unwrap(); + klass.add_pad_template(sink_pad_template); + + let caps = gst::Caps::builder("application/ogg").build(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps).unwrap(); + klass.add_pad_template(src_pad_template); + } +} + +impl ObjectImpl for VideoConv { + glib_object_impl!(); + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::().unwrap(); + element.add_pad(&self.sinkpad).unwrap(); + element.add_pad(&self.srcpad).unwrap(); + } +} + +impl ElementImpl for VideoConv { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange + ) -> Result { + + gst_log!(CAT, obj: element, "State transition: {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + /* do runtime setup */ + + let new_state = VideoConvState::new().map_err(|err| { + err.log(); + return gst::StateChangeError; + })?; + + let mut state = self.state.lock().unwrap(); + assert!((*state).is_none()); + *state = Some(new_state); + }, + + gst::StateChange::ReadyToNull => { + /* do runtime teardown */ + + let _ = self.state.lock().unwrap().take(); // dispose of state + }, + + _ => (), + }; + + self.parent_change_state(element, transition) + + /* XXX on ReadyToNull, sodium drops state _again_ here... why? */ + } +} + +struct StreamSerializer<'a> { + stream: &'a Vec, + cur_idx: usize, +} + +impl<'a> StreamSerializer<'a> { + fn new(stream: &'a Vec) -> Self { + StreamSerializer { + stream, + cur_idx: 0, + } + } +} + +impl <'a> Read for StreamSerializer<'a> { + fn read(&mut self, out: &mut [u8]) -> io::Result { + if self.cur_idx >= self.stream.len() { + return Ok(0) + } + + out[0..std::mem::size_of::()].copy_from_slice(&self.stream[self.cur_idx].to_le_bytes()); + self.cur_idx += 1; + + Ok(std::mem::size_of::()) + } +} + +impl VideoConv { + fn get_range( + &self, + _pad: &gst::Pad, + _element: &gst::Element, + offset: u64, + in_buf: Option<&mut gst::BufferRef>, + requested_size: u32, + ) -> Result { + + let mut state = self.state.lock().unwrap(); + + let state = match &mut *state { + Some(s) => s, + None => { return Err(gst::FlowError::Error); } + }; + + let ups_offset = self.duration_ours_to_upstream(&state, offset).unwrap(); + let ups_requested_size = self.duration_ours_to_upstream(&state, requested_size as u64).unwrap() as u32; + + /* read and ignore upstream bytes */ + self.sinkpad.pull_range(ups_offset, ups_requested_size)?; + + match in_buf { + Some(buf) => { + let readed; + + { + let mut map = buf.map_writable().unwrap(); + readed = state.fill_buffer(offset as usize, map.as_mut()) + .map_err(|e| { e.log(); gst::FlowError::Error })?; + } + + if readed > 0 || buf.get_size() == 0 { + buf.set_size(readed); + return Ok(gst::PadGetRangeSuccess::FilledBuffer); + } + + Err(gst::FlowError::Eos) + }, + + None => { + /* XXX: can we use a buffer cache here? */ + let b = gst::Buffer::with_size(requested_size as usize) + .map_err(|_| gst::FlowError::Error)?; + + let mut map = b.into_mapped_buffer_writable().unwrap(); + + let readed = state.fill_buffer(offset as usize, map.as_mut()) + .map_err(|e| { e.log(); gst::FlowError::Error })?; + + let mut b = map.into_buffer(); + + if readed > 0 || b.get_size() == 0 { + b.get_mut().unwrap().set_size(readed); + return Ok(gst::PadGetRangeSuccess::NewBuffer(b)); + } + + Err(gst::FlowError::Eos) + } + } + } + + fn sink_event( + &self, + pad: &gst::Pad, + element: &gst::Element, + event: gst::Event + ) -> bool { + gst_log!(CAT, obj:pad, "Got an event {:?}", event); + match event.view() { + EventView::Caps(_) => { + let caps = gst::Caps::builder("application/ogg").build(); + self.srcpad.push_event(gst::event::Caps::new(&caps)) + } + _ => pad.event_default(Some(element), event) + } + } + + fn query_upstream_duration(&self, state: &mut VideoConvState) { + let mut query = gst::query::Duration::new(gst::Format::Bytes); + + if self.sinkpad.peer_query(&mut query) { + /* XXX: what? */ + let res = query.get_result(); + drop(res); + state.upstream_duration = match query.get_result() { + gst::format::GenericFormattedValue::Bytes(b) => + *b, + _ => None, + } + }else{ + gst_warning!(CAT, "upstream duration query failure"); + } + } + + fn duration_ours_to_upstream(&self, state: &VideoConvState, pos: u64) -> Option { + if let Some(our) = state.our_duration { + if let Some(upstream) = state.upstream_duration { + return Some(pos * upstream / our); + } + } + None + } + + fn src_query( + &self, + pad: &gst::Pad, + element: &gst::Element, + query: &mut gst::QueryRef) -> bool + { + gst_log!(CAT, obj: pad, "got query: {:?}", query); + match query.view_mut() { + QueryView::Scheduling(mut q) => { + let mut peer_query = gst::query::Scheduling::new(); + let res = self.sinkpad.peer_query(&mut peer_query); + if ! res { + return res; + } + + let (flags, min, max, align) = peer_query.get_result(); + + q.set(flags, min, max, align); + q.add_scheduling_modes(&[gst::PadMode::Pull]); + true + }, + QueryView::Duration(ref mut q) => { + + let mut state = self.state.lock().unwrap(); + + let mut state = match &mut *state { + Some(s) => s, + None => { return false; } + }; + + if let None = state.upstream_duration { + self.query_upstream_duration(&mut state); + } + + if let Some(sz) = state.our_duration { + if q.get_format() == gst::Format::Bytes { + q.set(gst::format::Bytes::from(sz)); + return true + } + } + + false + } + _ => pad.query_default(Some(element), query) + } + } + + fn hash_upstream_data(&self) -> io::Result { + murmur3_128(&mut PadReader::new_with_stride(&self.sinkpad, HASH_STRIDE), HASH_SEED) + } + + fn dump_upstream_data(&self, hash: u128) -> io::Result<()> { + let mut db = (*DUMP_FOZDB).lock().unwrap(); + let db = match &mut *db { + Some(d) => d, + None => { gst_error!(CAT, "Unable to open fozdb!"); return Err(io::Error::new(io::ErrorKind::Other, "unable to open fozdb")); }, + }; + + let mut chunks = Vec::::new(); + + let mut reader = PadReader::new(&self.sinkpad); + + let mut buf = box_array![0u8; HASH_CHUNK_SIZE]; + loop { + let readed = reader.read(&mut *buf)?; + if readed == 0 { + break; + } + + let chunk_hash = murmur3_128(&mut BufferedReader::new(&*buf, readed), HASH_SEED)?; + chunks.push(chunk_hash); + + db.write_entry(VIDEOCONV_FOZ_TAG_VIDEODATA, chunk_hash, &mut BufferedReader::new(&*buf, readed), fossilize::CRCCheck::WithCRC) + .map_err(|e| { gst_warning!(CAT, "Error writing video data to fozdb: {:?}", e); io::Error::new(io::ErrorKind::Other, "error writing video data to fozdb") } )?; + } + + db.write_entry(VIDEOCONV_FOZ_TAG_STREAM, hash, &mut StreamSerializer::new(&chunks), fossilize::CRCCheck::WithCRC) + .map_err(|e| { gst_warning!(CAT, "Error writing stream data to fozdb: {:?}", e); io::Error::new(io::ErrorKind::Other, "error writing stream data to fozdb") } )?; + + Ok(()) + } + + fn init_transcode( + &self, + state: &mut VideoConvState + ) -> Result<(), gst::LoggableError> { + + let hash = self.hash_upstream_data(); + + if let Ok(hash) = hash { + if !state.begin_transcode(hash) { + self.dump_upstream_data(hash).map_err(|_| gst_loggable_error!(CAT, "Dumping file to disk failed"))?; + } + } + + Ok(()) + } + + fn src_activatemode( + &self, + _pad: &gst::Pad, + _element: &gst::Element, + mode: gst::PadMode, + active: bool + ) -> Result<(), gst::LoggableError> { + self.sinkpad + .activate_mode(mode, active)?; + + if mode == gst::PadMode::Pull { + let mut state = self.state.lock().unwrap(); + + let mut state = match &mut *state { + Some(s) => s, + None => { return Err(gst_loggable_error!(CAT, "VideoConv not yet in READY state?")); } + }; + + /* once we're initted in pull mode, we can start transcoding */ + self.init_transcode(&mut state)?; + } + + Ok(()) + } + +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "protonvideoconverter", + gst::Rank::Marginal, + VideoConv::get_type()) +} diff --git a/proton b/proton index e7435650..32203a1a 100755 --- a/proton +++ b/proton @@ -441,6 +441,12 @@ class Session: self.env["GST_PLUGIN_SYSTEM_PATH_1_0"] = g_proton.lib64_dir + "gstreamer-1.0" + ":" + g_proton.lib_dir + "gstreamer-1.0" self.env["WINE_GST_REGISTRY_DIR"] = g_compatdata.path("gstreamer-1.0/") + if "STEAM_COMPAT_MEDIA_PATH" in os.environ: + self.env["MEDIACONV_AUDIO_DUMP_FILE"] = os.environ["STEAM_COMPAT_MEDIA_PATH"] + "/audio.foz" + self.env["MEDIACONV_AUDIO_TRANSCODED_FILE"] = os.environ["STEAM_COMPAT_MEDIA_PATH"] + "/transcoded_audio.foz" + self.env["MEDIACONV_VIDEO_DUMP_FILE"] = os.environ["STEAM_COMPAT_MEDIA_PATH"] + "/video.foz" + self.env["MEDIACONV_VIDEO_TRANSCODED_FILE"] = os.environ["STEAM_COMPAT_MEDIA_PATH"] + "/transcoded_video.foz" + if "PATH" in os.environ: self.env["PATH"] = g_proton.bin_dir + ":" + os.environ["PATH"] else: @@ -585,6 +591,18 @@ class Session: f.write("\tSTEAM_COMPAT_CLIENT_INSTALL_PATH=\"" + self.env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] + "\" \\\n") if "WINE_LARGE_ADDRESS_AWARE" in self.env: f.write("\tWINE_LARGE_ADDRESS_AWARE=\"" + self.env["WINE_LARGE_ADDRESS_AWARE"] + "\" \\\n") + if "GST_PLUGIN_SYSTEM_PATH_1_0" in self.env: + f.write("\tGST_PLUGIN_SYSTEM_PATH_1_0=\"" + self.env["GST_PLUGIN_SYSTEM_PATH_1_0"] + "\" \\\n") + if "WINE_GST_REGISTRY_DIR" in self.env: + f.write("\tWINE_GST_REGISTRY_DIR=\"" + self.env["WINE_GST_REGISTRY_DIR"] + "\" \\\n") + if "MEDIACONV_AUDIO_DUMP_FILE" in self.env: + f.write("\tMEDIACONV_AUDIO_DUMP_FILE=\"" + self.env["MEDIACONV_AUDIO_DUMP_FILE"] + "\" \\\n") + if "MEDIACONV_AUDIO_TRANSCODED_FILE" in self.env: + f.write("\tMEDIACONV_AUDIO_TRANSCODED_FILE=\"" + self.env["MEDIACONV_AUDIO_TRANSCODED_FILE"] + "\" \\\n") + if "MEDIACONV_VIDEO_DUMP_FILE" in self.env: + f.write("\tMEDIACONV_VIDEO_DUMP_FILE=\"" + self.env["MEDIACONV_VIDEO_DUMP_FILE"] + "\" \\\n") + if "MEDIACONV_VIDEO_TRANSCODED_FILE" in self.env: + f.write("\tMEDIACONV_VIDEO_TRANSCODED_FILE=\"" + self.env["MEDIACONV_VIDEO_TRANSCODED_FILE"] + "\" \\\n") def dump_dbg_scripts(self): exe_name = os.path.basename(sys.argv[2]) diff --git a/user_settings.sample.py b/user_settings.sample.py index 353c5da1..874c3fe5 100755 --- a/user_settings.sample.py +++ b/user_settings.sample.py @@ -18,6 +18,11 @@ user_settings = { "WINE_MONO_TRACE": "E:System.NotImplementedException", #"MONO_LOG_LEVEL": "info", + #general purpose media logging +# "GST_DEBUG": "4", + #or, verbose converter logging (may impact playback performance): +# "GST_DEBUG": "4,protonaudioconverter:6,protonaudioconverterbin:6,protonvideoconverter:6", + #Enable DXVK's HUD # "DXVK_HUD": "devinfo,fps", diff --git a/vagrant-user-setup.sh b/vagrant-user-setup.sh index c0bd53a8..f0b73e8c 100755 --- a/vagrant-user-setup.sh +++ b/vagrant-user-setup.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + #build and install recent mingw-w64 if [ ! -e "$HOME/.local/bin/x86_64-w64-mingw32-gcc" ]; then mkdir -p $HOME/mingw-w64-build/ @@ -7,3 +9,25 @@ if [ ! -e "$HOME/.local/bin/x86_64-w64-mingw32-gcc" ]; then #clean up the build tree, this takes up like 6GB rm -rf $HOME/mingw-w64-build/ fi + +#install rust with x86_64 and i686 targets +RUST_VERSION="1.44.1" +RUST_CARGO_HOME=$HOME/rust/cargo +RUST_RUSTUP_HOME=$HOME/rust/rustup +if [ ! -e "$RUST_CARGO_HOME" ]; then + + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > $HOME/rustup-install.sh + + #64-bit toolchain + CARGO_HOME=$RUST_CARGO_HOME RUSTUP_HOME=$RUST_RUSTUP_HOME \ + sh $HOME/rustup-install.sh -y --no-modify-path \ + --default-host "x86_64-unknown-linux-gnu" \ + --default-toolchain "$RUST_VERSION-x86_64-unknown-linux-gnu" \ + -t "i686-unknown-linux-gnu" + + cat > $HOME/.local/bin/cargo <