Commit 6e5606f8 authored by Chris Cunningham's avatar Chris Cunningham Committed by Chromium LUCI CQ

Implement VideoDecoder.isConfigSupported()

API and motivations described here:
https://github.com/WICG/web-codecs/issues/98

Spec PR here:
https://github.com/WICG/web-codecs/pull/120

Bug: 1141707
Test: new layout tests
Change-Id: I3e53da5961119b4b36ec76666ad1a67cca19b963
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2612684
Commit-Queue: Chrome Cunningham <chcunningham@chromium.org>
Reviewed-by: default avatarThomas Guilbert <tguilbert@chromium.org>
Cr-Commit-Position: refs/heads/master@{#841376}
parent a98c3390
......@@ -16,6 +16,8 @@
#include "media/media_buildflags.h"
#include "third_party/blink/public/mojom/web_feature/web_feature.mojom-blink.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_encoded_video_chunk.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_decoder_config.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
......@@ -26,6 +28,7 @@
#include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/to_v8.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
......@@ -39,6 +42,125 @@
namespace blink {
bool ParseCodecString(const String& codec_string,
media::VideoType& out_video_type,
String& out_console_message) {
bool is_codec_ambiguous = true;
media::VideoCodec codec = media::kUnknownVideoCodec;
media::VideoCodecProfile profile = media::VIDEO_CODEC_PROFILE_UNKNOWN;
media::VideoColorSpace color_space = media::VideoColorSpace::REC709();
uint8_t level = 0;
bool parse_succeeded =
media::ParseVideoCodecString("", codec_string.Utf8(), &is_codec_ambiguous,
&codec, &profile, &level, &color_space);
if (!parse_succeeded) {
out_console_message = "Failed to parse codec string.";
return false;
}
if (is_codec_ambiguous) {
out_console_message = "Codec string is ambiguous.";
return false;
}
out_video_type = {codec, profile, level, color_space};
return true;
}
bool IsValidConfig(const VideoDecoderConfig& config,
media::VideoType& out_video_type,
String& out_console_message) {
if (!ParseCodecString(config.codec(), out_video_type, out_console_message))
return false;
if (config.hasCodedWidth()) {
if (config.codedWidth() == 0) {
out_console_message =
"Invalid codedWidth. Value must be greater than zero.";
return false;
}
uint32_t crop_left = config.hasCropLeft() ? config.cropLeft() : 0;
uint32_t crop_width =
config.hasCropWidth() ? config.cropWidth() : config.codedWidth();
if (crop_width == 0) {
out_console_message =
"Invalid cropWidth. Value must be greater than zero.";
return false;
}
if (crop_left + crop_width > config.codedWidth()) {
out_console_message =
"Invalid cropLeft + cropWidth. Sum must not exceed codedWidth.";
return false;
}
} else { // !config.hasCodedWidth()
if (config.hasCropLeft()) {
out_console_message =
"Invalid config. cropLeft specified without codedWidth.";
return false;
}
if (config.hasCropWidth()) {
out_console_message =
"Invalid config. cropWidth specified without codedWidth.";
return false;
}
}
if (config.hasCodedHeight()) {
if (config.codedHeight() == 0) {
out_console_message =
"Invalid codedHeight. Value must be greater than zero.";
return false;
}
uint32_t crop_top = config.hasCropTop() ? config.cropTop() : 0;
uint32_t crop_height =
config.hasCropHeight() ? config.cropHeight() : config.codedHeight();
if (crop_height == 0) {
out_console_message =
"Invalid cropHeight. Value must be greater than zero.";
return false;
}
if (crop_top + crop_height > config.codedHeight()) {
out_console_message =
"Invalid cropTop + cropHeight. Sum must not exceed codedHeight.";
return false;
}
} else { // !config.hasCodedHeight()
if (config.hasCropTop()) {
out_console_message =
"Invalid config. cropTop specified without codedHeight.";
return false;
}
if (config.hasCropHeight()) {
out_console_message =
"Invalid config. cropHeight specified without codedHeight.";
return false;
}
}
if (config.hasDisplayWidth() && config.displayWidth() == 0) {
out_console_message =
"Invalid displayWidth. Value must be greater than zero.";
return false;
}
if (config.hasDisplayHeight() && config.displayHeight() == 0) {
out_console_message =
"Invalid displayHeight. Value must be greater than zero.";
return false;
}
return true;
}
// static
std::unique_ptr<VideoDecoderTraits::MediaDecoderType>
VideoDecoderTraits::CreateDecoder(
......@@ -95,6 +217,24 @@ VideoDecoder* VideoDecoder::Create(ScriptState* script_state,
exception_state);
}
// static
ScriptPromise VideoDecoder::isConfigSupported(ScriptState* script_state,
const VideoDecoderConfig* config,
ExceptionState& exception_state) {
media::VideoType video_type;
String console_message;
if (!IsValidConfig(*config, video_type, console_message)) {
exception_state.ThrowTypeError(console_message);
return ScriptPromise();
}
// TODO(https://crbug.com/1164013): Add async checks for hardware support upon
// adding "acceleration" options to the config.
bool is_supported = media::IsSupportedVideoType(video_type);
return ScriptPromise::Cast(script_state, ToV8(is_supported, script_state));
}
// static
CodecConfigEval VideoDecoder::MakeMediaVideoDecoderConfig(
const ConfigType& config,
......@@ -104,29 +244,10 @@ CodecConfigEval VideoDecoder::MakeMediaVideoDecoderConfig(
std::unique_ptr<media::mp4::AVCDecoderConfigurationRecord>& out_h264_avcc,
#endif // BUILDFLAG(USE_PROPRIETARY_CODECS)
String& out_console_message) {
bool is_codec_ambiguous = true;
media::VideoCodec codec = media::kUnknownVideoCodec;
media::VideoCodecProfile profile = media::VIDEO_CODEC_PROFILE_UNKNOWN;
media::VideoColorSpace color_space = media::VideoColorSpace::REC709();
uint8_t level = 0;
bool parse_succeeded = media::ParseVideoCodecString(
"", config.codec().Utf8(), &is_codec_ambiguous, &codec, &profile, &level,
&color_space);
media::VideoType video_type;
if (!parse_succeeded) {
out_console_message = "Failed to parse codec string.";
if (!IsValidConfig(config, video_type, out_console_message))
return CodecConfigEval::kInvalid;
}
if (is_codec_ambiguous) {
out_console_message = "Codec string is ambiguous.";
return CodecConfigEval::kInvalid;
}
if (!media::IsSupportedVideoType({codec, profile, level, color_space})) {
out_console_message = "Configuration is not supported.";
return CodecConfigEval::kUnsupported;
}
// TODO(sandersd): Can we allow shared ArrayBuffers?
std::vector<uint8_t> extra_data;
......@@ -147,7 +268,7 @@ CodecConfigEval VideoDecoder::MakeMediaVideoDecoderConfig(
}
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
if (codec == media::kCodecH264) {
if (video_type.codec == media::kCodecH264) {
if (extra_data.empty()) {
out_console_message =
"H.264 configuration must include an avcC description.";
......@@ -169,7 +290,7 @@ CodecConfigEval VideoDecoder::MakeMediaVideoDecoderConfig(
out_h264_converter.reset();
}
#else
if (codec == media::kCodecH264) {
if (video_type.codec == media::kCodecH264) {
out_console_message = "H.264 decoding is not supported.";
return CodecConfigEval::kUnsupported;
}
......@@ -180,11 +301,11 @@ CodecConfigEval VideoDecoder::MakeMediaVideoDecoderConfig(
// match.
gfx::Size size = gfx::Size(1280, 720);
out_media_config.Initialize(codec, profile,
media::VideoDecoderConfig::AlphaMode::kIsOpaque,
color_space, media::kNoTransformation, size,
gfx::Rect(gfx::Point(), size), size, extra_data,
media::EncryptionScheme::kUnencrypted);
out_media_config.Initialize(
video_type.codec, video_type.profile,
media::VideoDecoderConfig::AlphaMode::kIsOpaque, video_type.color_space,
media::kNoTransformation, size, gfx::Rect(gfx::Point(), size), size,
extra_data, media::EncryptionScheme::kUnencrypted);
return CodecConfigEval::kSupported;
}
......
......@@ -48,6 +48,7 @@ class VideoDecoderConfig;
class VideoDecoderInit;
class VideoFrame;
class V8VideoFrameOutputCallback;
class ScriptPromise;
class MODULES_EXPORT VideoDecoderTraits {
public:
......@@ -86,6 +87,10 @@ class MODULES_EXPORT VideoDecoder : public DecoderTemplate<VideoDecoderTraits> {
const VideoDecoderInit*,
ExceptionState&);
static ScriptPromise isConfigSupported(ScriptState*,
const VideoDecoderConfig*,
ExceptionState&);
// For use by MediaSource and by ::MakeMediaConfig.
static CodecConfigEval MakeMediaVideoDecoderConfig(
const ConfigType& config,
......
......@@ -35,6 +35,9 @@
// TODO(sandersd): Consider emitting an event when this number decreases.
readonly attribute long decodeQueueSize;
// Which state the decoder is in, indicating which methods can be called.
readonly attribute CodecState state;
// Set the stream configuration for future decode() requests.
//
// The next decode request must be for a keyframe.
......@@ -73,6 +76,7 @@
// Not recoverable: make a new VideoDecoder if needed.
[RaisesException] void close();
// Which state the decoder is in, indicating which methods can be called.
readonly attribute CodecState state;
// Call prior to configure() to determine whether config will be supported.
[CallWith=ScriptState, RaisesException]
static Promise<boolean> isConfigSupported(VideoDecoderConfig config);
};
......@@ -4,7 +4,7 @@
// TODO(sandersd): Move metadata into a helper library.
// TODO(sandersd): Add H.264 decode test once there is an API to query for
// supported codecs.
let h264 = {
const h264 = {
async buffer() { return (await fetch('h264.mp4')).arrayBuffer(); },
codec: "avc1.64000c",
description: {offset: 7229, size: 46},
......@@ -20,7 +20,7 @@ let h264 = {
{offset: 6429, size: 281}]
};
let vp9 = {
const vp9 = {
async buffer() { return (await fetch('vp9.mp4')).arrayBuffer(); },
// TODO(sandersd): Verify that the file is actually level 1.
codec: "vp09.00.10.08",
......@@ -36,6 +36,88 @@ let vp9 = {
{offset: 5193, size: 159}]
};
const badCodecsList = [
'', // Empty codec
'bogus', // Non exsitent codec
'vorbis', // Audio codec
'vp9', // Ambiguous codec
'video/webm; codecs="vp9"' // Codec with mime type
]
const invalidConfigs = [
{
comment: 'Emtpy codec',
config: {codec: ''},
},
{
comment: 'Unrecognized codec',
config: {codec: 'bogus'},
},
{
comment: 'Audio codec',
config: {codec: 'vorbis'},
},
{
comment: 'Ambiguous codec',
config: {codec: 'vp9'},
},
{
comment: 'Codec with MIME type',
config: {codec: 'video/webm; codecs="vp8"'},
},
{
comment: 'Zero coded size',
config: {
codec: h264.codec,
codedWidth: 0,
codedHeight: 0,
},
},
{
comment: 'Out of bounds crop size caused by left/top offset',
config: {
codec: h264.codec,
codedWidth: 1920,
codedHeight: 1088,
cropLeft: 10,
cropTop: 10,
// When unspecified, these default to coded dimensions
// cropWidth: 1920,
// cropHeight: 1088
},
},
{
comment: 'Out of bounds crop size',
config: {
codec: h264.codec,
codedWidth: 1920,
codedHeight: 1088,
cropLeft: 10,
cropTop: 10,
cropWidth: 1920,
cropHeight: 1088,
},
},
{
comment: 'Way out of bounds crop size',
config: {
codec: h264.codec,
codedWidth: 1920,
codedHeight: 1088,
cropWidth: 4000,
cropHeight: 5000,
},
},
{
comment: 'Invalid display size',
config: {
codec: h264.codec,
displayWidth: 0,
displayHeight: 0,
},
},
] // invalidConfigs
function view(buffer, {offset, size}) {
return new Uint8Array(buffer, offset, size);
}
......@@ -48,6 +130,40 @@ function getFakeChunk() {
});
}
invalidConfigs.forEach(entry => {
promise_test(t => {
return promise_rejects_js(t, TypeError, VideoDecoder.isConfigSupported(entry.config));
}, 'Test that VideoDecoder.isConfigSupported() rejects invalid config:' + entry.comment);
});
invalidConfigs.forEach(entry => {
async_test(t => {
let codec = new VideoDecoder(getDefaultCodecInit(t));
assert_throws_js(TypeError, () => { codec.configure(entry.config); });
t.done();
}, 'Test that VideoDecoder.configure() rejects invalid config:' + entry.comment);
});
promise_test(t => {
return VideoDecoder.isConfigSupported({codec: vp9.codec});
}, 'Test VideoDecoder.isConfigSupported() with minimal valid config');
promise_test(t => {
// This config specifies a slight crop. H264 1080p content always crops
// because H264 coded dimensions are a multiple of 16 (e.g. 1088).
return VideoDecoder.isConfigSupported({
codec: h264.codec,
codedWidth: 1920,
codedHeight: 1088,
cropLeft: 0,
cropTop: 0,
cropWidth: 1920,
cropHeight: 1080,
displayWidth: 1920,
displayHeight: 1080
});
}, 'Test VideoDecoder.isConfigSupported() with valid expanded config');
promise_test(t => {
// VideoDecoderInit lacks required fields.
assert_throws_js(TypeError, () => { new VideoDecoder({}); });
......@@ -65,18 +181,12 @@ promise_test(t => {
promise_test(t => {
let decoder = new VideoDecoder(getDefaultCodecInit(t));
let badCodecsList = [
'', // Empty codec
'bogus', // Non exsitent codec
'vorbis', // Audio codec
'vp9', // Ambiguous codec
'video/webm; codecs="vp9"' // Codec with mime type
]
// TODO(chcunningham): Remove badCodecsList testing. It's now covered more
// extensively by other tests.
testConfigurations(decoder, { codec: vp9.codec }, badCodecsList);
return endAfterEventLoopTurn();
}, 'Test VideoDecoder.configure()');
}, 'Test VideoDecoder.configure() with various codec strings');
promise_test(async t => {
let buffer = await vp9.buffer();
......
......@@ -1725,6 +1725,7 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] getter isActive
[Worker] method constructor
[Worker] interface VideoDecoder
[Worker] static method isConfigSupported
[Worker] attribute @@toStringTag
[Worker] getter decodeQueueSize
[Worker] getter state
......
......@@ -8778,6 +8778,7 @@ interface ValidityState
getter valueMissing
method constructor
interface VideoDecoder
static method isConfigSupported
attribute @@toStringTag
getter decodeQueueSize
getter state
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment