1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "VPXDecoder.h"
8
9 #include <algorithm>
10
11 #include "BitReader.h"
12 #include "ByteWriter.h"
13 #include "ImageContainer.h"
14 #include "TimeUnits.h"
15 #include "gfx2DGlue.h"
16 #include "mozilla/PodOperations.h"
17 #include "mozilla/SyncRunnable.h"
18 #include "mozilla/TaskQueue.h"
19 #include "mozilla/Unused.h"
20 #include "nsError.h"
21 #include "prsystem.h"
22 #include "VideoUtils.h"
23
24 #undef LOG
25 #define LOG(arg, ...) \
26 DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, "::%s: " arg, __func__, \
27 ##__VA_ARGS__)
28
29 namespace mozilla {
30
31 using namespace gfx;
32 using namespace layers;
33
MimeTypeToCodec(const nsACString & aMimeType)34 static VPXDecoder::Codec MimeTypeToCodec(const nsACString& aMimeType) {
35 if (aMimeType.EqualsLiteral("video/vp8")) {
36 return VPXDecoder::Codec::VP8;
37 } else if (aMimeType.EqualsLiteral("video/vp9")) {
38 return VPXDecoder::Codec::VP9;
39 }
40 return VPXDecoder::Codec::Unknown;
41 }
42
InitContext(vpx_codec_ctx_t * aCtx,const VideoInfo & aInfo,const VPXDecoder::Codec aCodec,bool aLowLatency)43 static nsresult InitContext(vpx_codec_ctx_t* aCtx, const VideoInfo& aInfo,
44 const VPXDecoder::Codec aCodec, bool aLowLatency) {
45 int decode_threads = 2;
46
47 vpx_codec_iface_t* dx = nullptr;
48 if (aCodec == VPXDecoder::Codec::VP8) {
49 dx = vpx_codec_vp8_dx();
50 } else if (aCodec == VPXDecoder::Codec::VP9) {
51 dx = vpx_codec_vp9_dx();
52 if (aInfo.mDisplay.width >= 2048) {
53 decode_threads = 8;
54 } else if (aInfo.mDisplay.width >= 1024) {
55 decode_threads = 4;
56 }
57 }
58 decode_threads = std::min(decode_threads, PR_GetNumberOfProcessors());
59
60 vpx_codec_dec_cfg_t config;
61 config.threads = aLowLatency ? 1 : decode_threads;
62 config.w = config.h = 0; // set after decode
63
64 if (!dx || vpx_codec_dec_init(aCtx, dx, &config, 0)) {
65 return NS_ERROR_FAILURE;
66 }
67 return NS_OK;
68 }
69
VPXDecoder(const CreateDecoderParams & aParams)70 VPXDecoder::VPXDecoder(const CreateDecoderParams& aParams)
71 : mImageContainer(aParams.mImageContainer),
72 mImageAllocator(aParams.mKnowsCompositor),
73 mTaskQueue(new TaskQueue(
74 GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER), "VPXDecoder")),
75 mInfo(aParams.VideoConfig()),
76 mCodec(MimeTypeToCodec(aParams.VideoConfig().mMimeType)),
77 mLowLatency(
78 aParams.mOptions.contains(CreateDecoderParams::Option::LowLatency)) {
79 MOZ_COUNT_CTOR(VPXDecoder);
80 PodZero(&mVPX);
81 PodZero(&mVPXAlpha);
82 }
83
~VPXDecoder()84 VPXDecoder::~VPXDecoder() { MOZ_COUNT_DTOR(VPXDecoder); }
85
Shutdown()86 RefPtr<ShutdownPromise> VPXDecoder::Shutdown() {
87 RefPtr<VPXDecoder> self = this;
88 return InvokeAsync(mTaskQueue, __func__, [self]() {
89 vpx_codec_destroy(&self->mVPX);
90 vpx_codec_destroy(&self->mVPXAlpha);
91 return self->mTaskQueue->BeginShutdown();
92 });
93 }
94
Init()95 RefPtr<MediaDataDecoder::InitPromise> VPXDecoder::Init() {
96 if (NS_FAILED(InitContext(&mVPX, mInfo, mCodec, mLowLatency))) {
97 return VPXDecoder::InitPromise::CreateAndReject(
98 NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
99 }
100 if (mInfo.HasAlpha()) {
101 if (NS_FAILED(InitContext(&mVPXAlpha, mInfo, mCodec, mLowLatency))) {
102 return VPXDecoder::InitPromise::CreateAndReject(
103 NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
104 }
105 }
106 return VPXDecoder::InitPromise::CreateAndResolve(TrackInfo::kVideoTrack,
107 __func__);
108 }
109
Flush()110 RefPtr<MediaDataDecoder::FlushPromise> VPXDecoder::Flush() {
111 return InvokeAsync(mTaskQueue, __func__, []() {
112 return FlushPromise::CreateAndResolve(true, __func__);
113 });
114 }
115
ProcessDecode(MediaRawData * aSample)116 RefPtr<MediaDataDecoder::DecodePromise> VPXDecoder::ProcessDecode(
117 MediaRawData* aSample) {
118 MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
119
120 if (vpx_codec_err_t r = vpx_codec_decode(&mVPX, aSample->Data(),
121 aSample->Size(), nullptr, 0)) {
122 LOG("VPX Decode error: %s", vpx_codec_err_to_string(r));
123 return DecodePromise::CreateAndReject(
124 MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
125 RESULT_DETAIL("VPX error: %s", vpx_codec_err_to_string(r))),
126 __func__);
127 }
128
129 vpx_codec_iter_t iter = nullptr;
130 vpx_image_t* img;
131 vpx_image_t* img_alpha = nullptr;
132 bool alpha_decoded = false;
133 DecodedData results;
134
135 while ((img = vpx_codec_get_frame(&mVPX, &iter))) {
136 NS_ASSERTION(img->fmt == VPX_IMG_FMT_I420 || img->fmt == VPX_IMG_FMT_I444,
137 "WebM image format not I420 or I444");
138 NS_ASSERTION(!alpha_decoded,
139 "Multiple frames per packet that contains alpha");
140
141 if (aSample->AlphaSize() > 0) {
142 if (!alpha_decoded) {
143 MediaResult rv = DecodeAlpha(&img_alpha, aSample);
144 if (NS_FAILED(rv)) {
145 return DecodePromise::CreateAndReject(rv, __func__);
146 }
147 alpha_decoded = true;
148 }
149 }
150 // Chroma shifts are rounded down as per the decoding examples in the SDK
151 VideoData::YCbCrBuffer b;
152 b.mPlanes[0].mData = img->planes[0];
153 b.mPlanes[0].mStride = img->stride[0];
154 b.mPlanes[0].mHeight = img->d_h;
155 b.mPlanes[0].mWidth = img->d_w;
156 b.mPlanes[0].mSkip = 0;
157
158 b.mPlanes[1].mData = img->planes[1];
159 b.mPlanes[1].mStride = img->stride[1];
160 b.mPlanes[1].mSkip = 0;
161
162 b.mPlanes[2].mData = img->planes[2];
163 b.mPlanes[2].mStride = img->stride[2];
164 b.mPlanes[2].mSkip = 0;
165
166 if (img->fmt == VPX_IMG_FMT_I420) {
167 b.mPlanes[1].mHeight = (img->d_h + 1) >> img->y_chroma_shift;
168 b.mPlanes[1].mWidth = (img->d_w + 1) >> img->x_chroma_shift;
169
170 b.mPlanes[2].mHeight = (img->d_h + 1) >> img->y_chroma_shift;
171 b.mPlanes[2].mWidth = (img->d_w + 1) >> img->x_chroma_shift;
172 } else if (img->fmt == VPX_IMG_FMT_I444) {
173 b.mPlanes[1].mHeight = img->d_h;
174 b.mPlanes[1].mWidth = img->d_w;
175
176 b.mPlanes[2].mHeight = img->d_h;
177 b.mPlanes[2].mWidth = img->d_w;
178 } else {
179 LOG("VPX Unknown image format");
180 return DecodePromise::CreateAndReject(
181 MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
182 RESULT_DETAIL("VPX Unknown image format")),
183 __func__);
184 }
185 b.mYUVColorSpace = [&]() {
186 switch (img->cs) {
187 case VPX_CS_BT_601:
188 case VPX_CS_SMPTE_170:
189 case VPX_CS_SMPTE_240:
190 return gfx::YUVColorSpace::BT601;
191 case VPX_CS_BT_709:
192 return gfx::YUVColorSpace::BT709;
193 case VPX_CS_BT_2020:
194 return gfx::YUVColorSpace::BT2020;
195 default:
196 return DefaultColorSpace({img->d_w, img->d_h});
197 }
198 }();
199 b.mColorRange = img->range == VPX_CR_FULL_RANGE ? gfx::ColorRange::FULL
200 : gfx::ColorRange::LIMITED;
201
202 RefPtr<VideoData> v;
203 if (!img_alpha) {
204 v = VideoData::CreateAndCopyData(
205 mInfo, mImageContainer, aSample->mOffset, aSample->mTime,
206 aSample->mDuration, b, aSample->mKeyframe, aSample->mTimecode,
207 mInfo.ScaledImageRect(img->d_w, img->d_h), mImageAllocator);
208 } else {
209 VideoData::YCbCrBuffer::Plane alpha_plane;
210 alpha_plane.mData = img_alpha->planes[0];
211 alpha_plane.mStride = img_alpha->stride[0];
212 alpha_plane.mHeight = img_alpha->d_h;
213 alpha_plane.mWidth = img_alpha->d_w;
214 alpha_plane.mSkip = 0;
215 v = VideoData::CreateAndCopyData(
216 mInfo, mImageContainer, aSample->mOffset, aSample->mTime,
217 aSample->mDuration, b, alpha_plane, aSample->mKeyframe,
218 aSample->mTimecode, mInfo.ScaledImageRect(img->d_w, img->d_h));
219 }
220
221 if (!v) {
222 LOG("Image allocation error source %ux%u display %ux%u picture %ux%u",
223 img->d_w, img->d_h, mInfo.mDisplay.width, mInfo.mDisplay.height,
224 mInfo.mImage.width, mInfo.mImage.height);
225 return DecodePromise::CreateAndReject(
226 MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__), __func__);
227 }
228 results.AppendElement(std::move(v));
229 }
230 return DecodePromise::CreateAndResolve(std::move(results), __func__);
231 }
232
Decode(MediaRawData * aSample)233 RefPtr<MediaDataDecoder::DecodePromise> VPXDecoder::Decode(
234 MediaRawData* aSample) {
235 return InvokeAsync<MediaRawData*>(mTaskQueue, this, __func__,
236 &VPXDecoder::ProcessDecode, aSample);
237 }
238
Drain()239 RefPtr<MediaDataDecoder::DecodePromise> VPXDecoder::Drain() {
240 return InvokeAsync(mTaskQueue, __func__, [] {
241 return DecodePromise::CreateAndResolve(DecodedData(), __func__);
242 });
243 }
244
DecodeAlpha(vpx_image_t ** aImgAlpha,const MediaRawData * aSample)245 MediaResult VPXDecoder::DecodeAlpha(vpx_image_t** aImgAlpha,
246 const MediaRawData* aSample) {
247 vpx_codec_err_t r = vpx_codec_decode(&mVPXAlpha, aSample->AlphaData(),
248 aSample->AlphaSize(), nullptr, 0);
249 if (r) {
250 LOG("VPX decode alpha error: %s", vpx_codec_err_to_string(r));
251 return MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
252 RESULT_DETAIL("VPX decode alpha error: %s",
253 vpx_codec_err_to_string(r)));
254 }
255
256 vpx_codec_iter_t iter = nullptr;
257
258 *aImgAlpha = vpx_codec_get_frame(&mVPXAlpha, &iter);
259 NS_ASSERTION((*aImgAlpha)->fmt == VPX_IMG_FMT_I420 ||
260 (*aImgAlpha)->fmt == VPX_IMG_FMT_I444,
261 "WebM image format not I420 or I444");
262
263 return NS_OK;
264 }
265
266 /* static */
IsVPX(const nsACString & aMimeType,uint8_t aCodecMask)267 bool VPXDecoder::IsVPX(const nsACString& aMimeType, uint8_t aCodecMask) {
268 return ((aCodecMask & VPXDecoder::VP8) &&
269 aMimeType.EqualsLiteral("video/vp8")) ||
270 ((aCodecMask & VPXDecoder::VP9) &&
271 aMimeType.EqualsLiteral("video/vp9"));
272 }
273
274 /* static */
IsVP8(const nsACString & aMimeType)275 bool VPXDecoder::IsVP8(const nsACString& aMimeType) {
276 return IsVPX(aMimeType, VPXDecoder::VP8);
277 }
278
279 /* static */
IsVP9(const nsACString & aMimeType)280 bool VPXDecoder::IsVP9(const nsACString& aMimeType) {
281 return IsVPX(aMimeType, VPXDecoder::VP9);
282 }
283
284 /* static */
IsKeyframe(Span<const uint8_t> aBuffer,Codec aCodec)285 bool VPXDecoder::IsKeyframe(Span<const uint8_t> aBuffer, Codec aCodec) {
286 VPXStreamInfo info;
287 return GetStreamInfo(aBuffer, info, aCodec) && info.mKeyFrame;
288 }
289
290 /* static */
GetFrameSize(Span<const uint8_t> aBuffer,Codec aCodec)291 gfx::IntSize VPXDecoder::GetFrameSize(Span<const uint8_t> aBuffer,
292 Codec aCodec) {
293 VPXStreamInfo info;
294 if (!GetStreamInfo(aBuffer, info, aCodec)) {
295 return gfx::IntSize();
296 }
297 return info.mImage;
298 }
299
300 /* static */
GetDisplaySize(Span<const uint8_t> aBuffer,Codec aCodec)301 gfx::IntSize VPXDecoder::GetDisplaySize(Span<const uint8_t> aBuffer,
302 Codec aCodec) {
303 VPXStreamInfo info;
304 if (!GetStreamInfo(aBuffer, info, aCodec)) {
305 return gfx::IntSize();
306 }
307 return info.mDisplay;
308 }
309
310 /* static */
GetVP9Profile(Span<const uint8_t> aBuffer)311 int VPXDecoder::GetVP9Profile(Span<const uint8_t> aBuffer) {
312 VPXStreamInfo info;
313 if (!GetStreamInfo(aBuffer, info, Codec::VP9)) {
314 return -1;
315 }
316 return info.mProfile;
317 }
318
319 /* static */
GetStreamInfo(Span<const uint8_t> aBuffer,VPXDecoder::VPXStreamInfo & aInfo,Codec aCodec)320 bool VPXDecoder::GetStreamInfo(Span<const uint8_t> aBuffer,
321 VPXDecoder::VPXStreamInfo& aInfo, Codec aCodec) {
322 if (aBuffer.IsEmpty()) {
323 // Can't be good.
324 return false;
325 }
326
327 aInfo = VPXStreamInfo();
328
329 if (aCodec == Codec::VP8) {
330 aInfo.mKeyFrame = (aBuffer[0] & 1) ==
331 0; // frame type (0 for key frames, 1 for interframes)
332 if (!aInfo.mKeyFrame) {
333 // We can't retrieve the required information from interframes.
334 return true;
335 }
336 if (aBuffer.Length() < 10) {
337 return false;
338 }
339 uint8_t version = (aBuffer[0] >> 1) & 0x7;
340 if (version > 3) {
341 return false;
342 }
343 uint8_t start_code_byte_0 = aBuffer[3];
344 uint8_t start_code_byte_1 = aBuffer[4];
345 uint8_t start_code_byte_2 = aBuffer[5];
346 if (start_code_byte_0 != 0x9d || start_code_byte_1 != 0x01 ||
347 start_code_byte_2 != 0x2a) {
348 return false;
349 }
350 uint16_t width = (aBuffer[6] | aBuffer[7] << 8) & 0x3fff;
351 uint16_t height = (aBuffer[8] | aBuffer[9] << 8) & 0x3fff;
352
353 // aspect ratio isn't found in the VP8 frame header.
354 aInfo.mImage = aInfo.mDisplay = gfx::IntSize(width, height);
355 aInfo.mDisplayAspectRatio =
356 (float)(aInfo.mDisplay.Width()) / (float)(aInfo.mDisplay.Height());
357 return true;
358 }
359
360 BitReader br(aBuffer.Elements(), aBuffer.Length() * 8);
361 uint32_t frameMarker = br.ReadBits(2); // frame_marker
362 if (frameMarker != 2) {
363 // That's not a valid vp9 header.
364 return false;
365 }
366 uint32_t profile = br.ReadBits(1); // profile_low_bit
367 profile |= br.ReadBits(1) << 1; // profile_high_bit
368 if (profile == 3) {
369 profile += br.ReadBits(1); // reserved_zero
370 if (profile > 3) {
371 // reserved_zero wasn't zero.
372 return false;
373 }
374 }
375
376 aInfo.mProfile = profile;
377
378 bool show_existing_frame = br.ReadBits(1);
379 if (show_existing_frame) {
380 if (profile == 3 && aBuffer.Length() < 2) {
381 return false;
382 }
383 Unused << br.ReadBits(3); // frame_to_show_map_idx
384 return true;
385 }
386
387 if (aBuffer.Length() < 10) {
388 // Header too small;
389 return false;
390 }
391
392 aInfo.mKeyFrame = !br.ReadBits(1);
393 bool show_frame = br.ReadBits(1);
394 bool error_resilient_mode = br.ReadBits(1);
395
396 auto frame_sync_code = [&]() -> bool {
397 uint8_t frame_sync_byte_1 = br.ReadBits(8);
398 uint8_t frame_sync_byte_2 = br.ReadBits(8);
399 uint8_t frame_sync_byte_3 = br.ReadBits(8);
400 return frame_sync_byte_1 == 0x49 && frame_sync_byte_2 == 0x83 &&
401 frame_sync_byte_3 == 0x42;
402 };
403
404 auto color_config = [&]() -> bool {
405 aInfo.mBitDepth = 8;
406 if (profile >= 2) {
407 bool ten_or_twelve_bit = br.ReadBits(1);
408 aInfo.mBitDepth = ten_or_twelve_bit ? 12 : 10;
409 }
410 aInfo.mColorSpace = br.ReadBits(3);
411 if (aInfo.mColorSpace != 7 /* CS_RGB */) {
412 aInfo.mFullRange = br.ReadBits(1);
413 if (profile == 1 || profile == 3) {
414 aInfo.mSubSampling_x = br.ReadBits(1);
415 aInfo.mSubSampling_y = br.ReadBits(1);
416 if (br.ReadBits(1)) { // reserved_zero
417 return false;
418 };
419 } else {
420 aInfo.mSubSampling_x = true;
421 aInfo.mSubSampling_y = true;
422 }
423 } else {
424 aInfo.mFullRange = true;
425 if (profile == 1 || profile == 3) {
426 aInfo.mSubSampling_x = false;
427 aInfo.mSubSampling_y = false;
428 if (br.ReadBits(1)) { // reserved_zero
429 return false;
430 };
431 } else {
432 // sRGB color space is only available with VP9 profile 1.
433 return false;
434 }
435 }
436 return true;
437 };
438
439 auto frame_size = [&]() {
440 int32_t width = static_cast<int32_t>(br.ReadBits(16)) + 1;
441 int32_t height = static_cast<int32_t>(br.ReadBits(16)) + 1;
442 aInfo.mImage = gfx::IntSize(width, height);
443 };
444
445 auto render_size = [&]() {
446 bool render_and_frame_size_different = br.ReadBits(1);
447 if (render_and_frame_size_different) {
448 int32_t width = static_cast<int32_t>(br.ReadBits(16)) + 1;
449 int32_t height = static_cast<int32_t>(br.ReadBits(16)) + 1;
450 aInfo.mDisplay = gfx::IntSize(width, height);
451 } else {
452 aInfo.mDisplay = aInfo.mImage;
453 }
454 aInfo.mDisplayAspectRatio =
455 (float)(aInfo.mDisplay.Width()) / (float)(aInfo.mDisplay.Height());
456 };
457
458 if (aInfo.mKeyFrame) {
459 if (!frame_sync_code()) {
460 return false;
461 }
462 if (!color_config()) {
463 return false;
464 }
465 frame_size();
466 render_size();
467 } else {
468 bool intra_only = show_frame ? false : br.ReadBit();
469 if (!error_resilient_mode) {
470 Unused << br.ReadBits(2); // reset_frame_context
471 }
472 if (intra_only) {
473 if (!frame_sync_code()) {
474 return false;
475 }
476 if (profile > 0) {
477 if (!color_config()) {
478 return false;
479 }
480 } else {
481 aInfo.mColorSpace = 1; // CS_BT_601
482 aInfo.mSubSampling_x = true;
483 aInfo.mSubSampling_y = true;
484 aInfo.mBitDepth = 8;
485 }
486 Unused << br.ReadBits(8); // refresh_frame_flags
487 frame_size();
488 render_size();
489 }
490 }
491 return true;
492 }
493
494 // Ref: "VP Codec ISO Media File Format Binding, v1.0, 2017-03-31"
495 // <https://www.webmproject.org/vp9/mp4/>
496 //
497 // class VPCodecConfigurationBox extends FullBox('vpcC', version = 1, 0)
498 // {
499 // VPCodecConfigurationRecord() vpcConfig;
500 // }
501 //
502 // aligned (8) class VPCodecConfigurationRecord {
503 // unsigned int (8) profile;
504 // unsigned int (8) level;
505 // unsigned int (4) bitDepth;
506 // unsigned int (3) chromaSubsampling;
507 // unsigned int (1) videoFullRangeFlag;
508 // unsigned int (8) colourPrimaries;
509 // unsigned int (8) transferCharacteristics;
510 // unsigned int (8) matrixCoefficients;
511 // unsigned int (16) codecIntializationDataSize;
512 // unsigned int (8)[] codecIntializationData;
513 // }
514
515 /* static */
GetVPCCBox(MediaByteBuffer * aDestBox,const VPXStreamInfo & aInfo)516 void VPXDecoder::GetVPCCBox(MediaByteBuffer* aDestBox,
517 const VPXStreamInfo& aInfo) {
518 ByteWriter<BigEndian> writer(*aDestBox);
519
520 int chroma = [&]() {
521 if (aInfo.mSubSampling_x && aInfo.mSubSampling_y) {
522 return 1; // 420 Colocated;
523 }
524 if (aInfo.mSubSampling_x && !aInfo.mSubSampling_y) {
525 return 2; // 422
526 }
527 if (!aInfo.mSubSampling_x && !aInfo.mSubSampling_y) {
528 return 3; // 444
529 }
530 // This indicates 4:4:0 subsampling, which is not expressable in the
531 // 'vpcC' box. Default to 4:2:0.
532 return 1;
533 }();
534
535 MOZ_ALWAYS_TRUE(writer.WriteU32(1 << 24)); // version & flag
536 MOZ_ALWAYS_TRUE(writer.WriteU8(aInfo.mProfile)); // profile
537 MOZ_ALWAYS_TRUE(writer.WriteU8(10)); // level set it to 1.0
538 MOZ_ALWAYS_TRUE(writer.WriteU8(
539 (0xF & aInfo.mBitDepth) << 4 | (0x7 & chroma) << 1 |
540 (0x1 & aInfo.mFullRange))); // bitdepth (4 bits), chroma (3 bits),
541 // video full/restrice range (1 bit)
542 MOZ_ALWAYS_TRUE(writer.WriteU8(2)); // color primaries: unknown
543 MOZ_ALWAYS_TRUE(writer.WriteU8(2)); // transfer characteristics: unknown
544 MOZ_ALWAYS_TRUE(writer.WriteU8(2)); // matrix coefficient: unknown
545 MOZ_ALWAYS_TRUE(
546 writer.WriteU16(0)); // codecIntializationDataSize (must be 0 for VP9)
547 }
548
549 } // namespace mozilla
550 #undef LOG
551