Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(15)

Side by Side Diff: media/base/android/media_drm_bridge.cc

Issue 962793005: Adds MediaClientAndroid to support embedder/MediaDrmBridge interaction. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: native API example Created 5 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "media/base/android/media_drm_bridge.h" 5 #include "media/base/android/media_drm_bridge.h"
6 6
7 #include <algorithm> 7 #include <algorithm>
8 8
9 #include "base/android/build_info.h" 9 #include "base/android/build_info.h"
10 #include "base/android/jni_array.h" 10 #include "base/android/jni_array.h"
11 #include "base/android/jni_string.h" 11 #include "base/android/jni_string.h"
12 #include "base/callback_helpers.h" 12 #include "base/callback_helpers.h"
13 #include "base/containers/hash_tables.h" 13 #include "base/containers/hash_tables.h"
14 #include "base/lazy_instance.h" 14 #include "base/lazy_instance.h"
15 #include "base/location.h" 15 #include "base/location.h"
16 #include "base/logging.h" 16 #include "base/logging.h"
17 #include "base/message_loop/message_loop_proxy.h" 17 #include "base/message_loop/message_loop_proxy.h"
18 #include "base/stl_util.h"
18 #include "base/strings/string_util.h" 19 #include "base/strings/string_util.h"
19 #include "base/sys_byteorder.h" 20 #include "base/sys_byteorder.h"
20 #include "base/sys_info.h" 21 #include "base/sys_info.h"
21 #include "jni/MediaDrmBridge_jni.h" 22 #include "jni/MediaDrmBridge_jni.h"
22 #include "media/base/cdm_key_information.h" 23 #include "media/base/cdm_key_information.h"
23 24
24 #include "widevine_cdm_version.h" // In SHARED_INTERMEDIATE_DIR. 25 #include "widevine_cdm_version.h" // In SHARED_INTERMEDIATE_DIR.
25 26
26 using base::android::AttachCurrentThread; 27 using base::android::AttachCurrentThread;
27 using base::android::ConvertUTF8ToJavaString; 28 using base::android::ConvertUTF8ToJavaString;
28 using base::android::ConvertJavaStringToUTF8; 29 using base::android::ConvertJavaStringToUTF8;
29 using base::android::JavaByteArrayToByteVector; 30 using base::android::JavaByteArrayToByteVector;
30 using base::android::ScopedJavaLocalRef; 31 using base::android::ScopedJavaLocalRef;
31 32
32 namespace media { 33 namespace media {
33 34
34 namespace { 35 namespace {
35 36
36 // DrmBridge supports session expiration event but doesn't provide detailed 37 // DrmBridge supports session expiration event but doesn't provide detailed
37 // status for each key ID, which is required by the EME spec. Use a dummy key ID 38 // status for each key ID, which is required by the EME spec. Use a dummy key ID
38 // here to report session expiration info. 39 // here to report session expiration info.
39 const char kDummyKeyId[] = "Dummy Key Id"; 40 const char kDummyKeyId[] = "Dummy Key Id";
40 41
42 /*
41 uint32 ReadUint32(const uint8_t* data) { 43 uint32 ReadUint32(const uint8_t* data) {
42 uint32 value = 0; 44 uint32 value = 0;
43 for (int i = 0; i < 4; ++i) 45 for (int i = 0; i < 4; ++i)
44 value = (value << 8) | data[i]; 46 value = (value << 8) | data[i];
45 return value; 47 return value;
46 } 48 }
47 49
48 uint64 ReadUint64(const uint8_t* data) { 50 uint64 ReadUint64(const uint8_t* data) {
49 uint64 value = 0; 51 uint64 value = 0;
50 for (int i = 0; i < 8; ++i) 52 for (int i = 0; i < 8; ++i)
51 value = (value << 8) | data[i]; 53 value = (value << 8) | data[i];
52 return value; 54 return value;
53 } 55 }
56 */
54 57
55 // Returns string session ID from jbyteArray (byte[] in Java). 58 // Returns string session ID from jbyteArray (byte[] in Java).
56 std::string GetSessionId(JNIEnv* env, jbyteArray j_session_id) { 59 std::string GetSessionId(JNIEnv* env, jbyteArray j_session_id) {
57 std::vector<uint8> session_id_vector; 60 std::vector<uint8> session_id_vector;
58 JavaByteArrayToByteVector(env, j_session_id, &session_id_vector); 61 JavaByteArrayToByteVector(env, j_session_id, &session_id_vector);
59 return std::string(session_id_vector.begin(), session_id_vector.end()); 62 return std::string(session_id_vector.begin(), session_id_vector.end());
60 } 63 }
61 64
65 /*
62 // The structure of an ISO CENC Protection System Specific Header (PSSH) box is 66 // The structure of an ISO CENC Protection System Specific Header (PSSH) box is
63 // as follows. (See ISO/IEC FDIS 23001-7:2011(E).) 67 // as follows. (See ISO/IEC FDIS 23001-7:2011(E).)
64 // Note: ISO boxes use big-endian values. 68 // Note: ISO boxes use big-endian values.
65 // 69 //
66 // PSSH { 70 // PSSH {
67 // uint32 Size 71 // uint32 Size
68 // uint32 Type 72 // uint32 Type
69 // uint64 LargeSize # Field is only present if value(Size) == 1. 73 // uint64 LargeSize # Field is only present if value(Size) == 1.
70 // uint32 VersionAndFlags 74 // uint32 VersionAndFlags
71 // uint8[16] SystemId 75 // uint8[16] SystemId
72 // uint32 DataSize 76 // uint32 DataSize
73 // uint8[DataSize] Data 77 // uint8[DataSize] Data
74 // } 78 // }
75 const int kBoxHeaderSize = 8; // Box's header contains Size and Type. 79 const int kBoxHeaderSize = 8; // Box's header contains Size and Type.
76 const int kBoxLargeSizeSize = 8; 80 const int kBoxLargeSizeSize = 8;
77 const int kPsshVersionFlagSize = 4; 81 const int kPsshVersionFlagSize = 4;
78 const int kPsshSystemIdSize = 16; 82 const int kPsshSystemIdSize = 16;
79 const int kPsshDataSizeSize = 4; 83 const int kPsshDataSizeSize = 4;
80 const uint32 kTencType = 0x74656e63; 84 const uint32 kTencType = 0x74656e63;
81 const uint32 kPsshType = 0x70737368; 85 const uint32 kPsshType = 0x70737368;
86 */
82 const uint8 kWidevineUuid[16] = { 87 const uint8 kWidevineUuid[16] = {
83 0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE, 88 0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE,
84 0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED }; 89 0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED };
85 90
86 typedef std::vector<uint8> UUID; 91 typedef std::vector<uint8> UUID;
87 92
93 /*
88 // Tries to find a PSSH box whose "SystemId" is |uuid| in |data|, parses the 94 // Tries to find a PSSH box whose "SystemId" is |uuid| in |data|, parses the
89 // "Data" of the box and put it in |pssh_data|. Returns true if such a box is 95 // "Data" of the box and put it in |pssh_data|. Returns true if such a box is
90 // found and successfully parsed. Returns false otherwise. 96 // found and successfully parsed. Returns false otherwise.
91 // Notes: 97 // Notes:
92 // 1, If multiple PSSH boxes are found,the "Data" of the first matching PSSH box 98 // 1, If multiple PSSH boxes are found,the "Data" of the first matching PSSH box
93 // will be set in |pssh_data|. 99 // will be set in |pssh_data|.
94 // 2, Only PSSH and TENC boxes are allowed in |data|. TENC boxes are skipped. 100 // 2, Only PSSH and TENC boxes are allowed in |data|. TENC boxes are skipped.
95 bool GetPsshData(const uint8* data, 101 bool GetPsshData(const uint8* data,
96 int data_size, 102 int data_size,
97 const UUID& uuid, 103 const UUID& uuid,
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after
162 168
163 if (box_end < cur + data_size) 169 if (box_end < cur + data_size)
164 return false; 170 return false;
165 171
166 pssh_data->assign(cur, cur + data_size); 172 pssh_data->assign(cur, cur + data_size);
167 return true; 173 return true;
168 } 174 }
169 175
170 return false; 176 return false;
171 } 177 }
178 */
172 179
173 class KeySystemUuidManager { 180 class KeySystemUuidManager {
174 public: 181 public:
175 KeySystemUuidManager(); 182 KeySystemUuidManager();
176 UUID GetUUID(const std::string& key_system); 183 UUID GetUUID(const std::string& key_system);
177 void AddMapping(const std::string& key_system, const UUID& uuid); 184 void AddMapping(const std::string& key_system, const UUID& uuid);
178 std::vector<std::string> GetPlatformKeySystemNames(); 185 std::vector<std::string> GetPlatformKeySystemNames();
179 186
180 private: 187 private:
181 typedef base::hash_map<std::string, UUID> KeySystemUuidMap; 188 typedef base::hash_map<std::string, UUID> KeySystemUuidMap;
(...skipping 79 matching lines...) Expand 10 before | Expand all | Expand 10 after
261 case MediaDrmBridge::SECURITY_LEVEL_NONE: 268 case MediaDrmBridge::SECURITY_LEVEL_NONE:
262 return ""; 269 return "";
263 case MediaDrmBridge::SECURITY_LEVEL_1: 270 case MediaDrmBridge::SECURITY_LEVEL_1:
264 return "L1"; 271 return "L1";
265 case MediaDrmBridge::SECURITY_LEVEL_3: 272 case MediaDrmBridge::SECURITY_LEVEL_3:
266 return "L3"; 273 return "L3";
267 } 274 }
268 return ""; 275 return "";
269 } 276 }
270 277
278 class DelegateList {
279 public:
280 DelegateList();
281
282 void RegisterDelegate(const std::string& key_system,
283 MediaDrmBridge::Delegate* delegate);
284 MediaDrmBridge::Delegate* GetDelegate(const std::string& key_system);
285
286 private:
287 std::hash_map<std::string, MediaDrmBridge::Delegate*> delegates_;
288 DISALLOW_COPY_AND_ASSIGN(DelegateList);
289 };
290
291 base::LazyInstance<DelegateList>::Leaky g_delegates =
292 LAZY_INSTANCE_INITIALIZER;
293
294 DelegateList::DelegateList() {
295 }
296
297 void DelegateList::RegisterDelegate(
298 const std::string& key_system, MediaDrmBridge::Delegate* delegate) {
299 DCHECK(!ContainsKey(delegates_, key_system));
300 delegates_[key_system] = delegate;
301 }
302
303 MediaDrmBridge::Delegate* DelegateList::GetDelegate(
304 const std::string& key_system) {
305 if (!ContainsKey(delegates_, key_system))
306 return nullptr;
307 return delegates_[key_system];
308 }
309
271 } // namespace 310 } // namespace
272 311
273 // Called by Java. 312 // Called by Java.
274 static void AddKeySystemUuidMapping(JNIEnv* env, 313 static void AddKeySystemUuidMapping(JNIEnv* env,
275 jclass clazz, 314 jclass clazz,
276 jstring j_key_system, 315 jstring j_key_system,
277 jobject j_buffer) { 316 jobject j_buffer) {
278 std::string key_system = ConvertJavaStringToUTF8(env, j_key_system); 317 std::string key_system = ConvertJavaStringToUTF8(env, j_key_system);
279 uint8* buffer = static_cast<uint8*>(env->GetDirectBufferAddress(j_buffer)); 318 uint8* buffer = static_cast<uint8*>(env->GetDirectBufferAddress(j_buffer));
280 UUID uuid(buffer, buffer + 16); 319 UUID uuid(buffer, buffer + 16);
281 g_key_system_uuid_manager.Get().AddMapping(key_system, uuid); 320 g_key_system_uuid_manager.Get().AddMapping(key_system, uuid);
xhwang 2015/03/23 04:59:47 Can we merge g_delegates and g_key_system_uuid_man
gunsch 2015/04/09 01:36:39 Done.
282 } 321 }
283 322
284 // static 323 // static
285 bool MediaDrmBridge::IsAvailable() { 324 bool MediaDrmBridge::IsAvailable() {
286 if (base::android::BuildInfo::GetInstance()->sdk_int() < 19) 325 if (base::android::BuildInfo::GetInstance()->sdk_int() < 19)
287 return false; 326 return false;
288 327
289 int32 os_major_version = 0; 328 int32 os_major_version = 0;
290 int32 os_minor_version = 0; 329 int32 os_minor_version = 0;
291 int32 os_bugfix_version = 0; 330 int32 os_bugfix_version = 0;
(...skipping 25 matching lines...) Expand all
317 } 356 }
318 357
319 // static 358 // static
320 bool MediaDrmBridge::IsKeySystemSupportedWithType( 359 bool MediaDrmBridge::IsKeySystemSupportedWithType(
321 const std::string& key_system, 360 const std::string& key_system,
322 const std::string& container_mime_type) { 361 const std::string& container_mime_type) {
323 DCHECK(!key_system.empty() && !container_mime_type.empty()); 362 DCHECK(!key_system.empty() && !container_mime_type.empty());
324 return IsKeySystemSupportedWithTypeImpl(key_system, container_mime_type); 363 return IsKeySystemSupportedWithTypeImpl(key_system, container_mime_type);
325 } 364 }
326 365
366 // static
367 void MediaDrmBridge::RegisterDelegate(const std::string& key_system,
368 Delegate* delegate) {
369 g_delegates.Get().RegisterDelegate(key_system, delegate);
370 }
371
327 bool MediaDrmBridge::RegisterMediaDrmBridge(JNIEnv* env) { 372 bool MediaDrmBridge::RegisterMediaDrmBridge(JNIEnv* env) {
328 return RegisterNativesImpl(env); 373 return RegisterNativesImpl(env);
329 } 374 }
330 375
331 MediaDrmBridge::MediaDrmBridge( 376 MediaDrmBridge::MediaDrmBridge(
332 const std::vector<uint8>& scheme_uuid, 377 const std::vector<uint8>& scheme_uuid,
333 const SessionMessageCB& session_message_cb, 378 const SessionMessageCB& session_message_cb,
334 const SessionClosedCB& session_closed_cb, 379 const SessionClosedCB& session_closed_cb,
335 const SessionErrorCB& session_error_cb, 380 const SessionErrorCB& session_error_cb,
336 const SessionKeysChangeCB& session_keys_change_cb) 381 const SessionKeysChangeCB& session_keys_change_cb,
382 Delegate* delegate)
337 : scheme_uuid_(scheme_uuid), 383 : scheme_uuid_(scheme_uuid),
384 delegate_(delegate),
338 session_message_cb_(session_message_cb), 385 session_message_cb_(session_message_cb),
339 session_closed_cb_(session_closed_cb), 386 session_closed_cb_(session_closed_cb),
340 session_error_cb_(session_error_cb), 387 session_error_cb_(session_error_cb),
341 session_keys_change_cb_(session_keys_change_cb) { 388 session_keys_change_cb_(session_keys_change_cb) {
342 JNIEnv* env = AttachCurrentThread(); 389 JNIEnv* env = AttachCurrentThread();
343 CHECK(env); 390 CHECK(env);
344 391
345 ScopedJavaLocalRef<jbyteArray> j_scheme_uuid = 392 ScopedJavaLocalRef<jbyteArray> j_scheme_uuid =
346 base::android::ToJavaByteArray(env, &scheme_uuid[0], scheme_uuid.size()); 393 base::android::ToJavaByteArray(env, &scheme_uuid[0], scheme_uuid.size());
347 j_media_drm_.Reset(Java_MediaDrmBridge_create( 394 j_media_drm_.Reset(Java_MediaDrmBridge_create(
(...skipping 17 matching lines...) Expand all
365 const SessionKeysChangeCB& session_keys_change_cb, 412 const SessionKeysChangeCB& session_keys_change_cb,
366 const SessionExpirationUpdateCB& /* session_expiration_update_cb */) { 413 const SessionExpirationUpdateCB& /* session_expiration_update_cb */) {
367 scoped_ptr<MediaDrmBridge> media_drm_bridge; 414 scoped_ptr<MediaDrmBridge> media_drm_bridge;
368 if (!IsAvailable()) 415 if (!IsAvailable())
369 return media_drm_bridge.Pass(); 416 return media_drm_bridge.Pass();
370 417
371 UUID scheme_uuid = g_key_system_uuid_manager.Get().GetUUID(key_system); 418 UUID scheme_uuid = g_key_system_uuid_manager.Get().GetUUID(key_system);
372 if (scheme_uuid.empty()) 419 if (scheme_uuid.empty())
373 return media_drm_bridge.Pass(); 420 return media_drm_bridge.Pass();
374 421
422 Delegate* delegate = g_delegates.Get().GetDelegate(key_system);
375 media_drm_bridge.reset(new MediaDrmBridge(scheme_uuid, session_message_cb, 423 media_drm_bridge.reset(new MediaDrmBridge(scheme_uuid, session_message_cb,
376 session_closed_cb, session_error_cb, 424 session_closed_cb, session_error_cb,
377 session_keys_change_cb)); 425 session_keys_change_cb, delegate));
378 426
379 if (media_drm_bridge->j_media_drm_.is_null()) 427 if (media_drm_bridge->j_media_drm_.is_null())
380 media_drm_bridge.reset(); 428 media_drm_bridge.reset();
381 429
382 return media_drm_bridge.Pass(); 430 return media_drm_bridge.Pass();
383 } 431 }
384 432
385 // static 433 // static
386 scoped_ptr<MediaDrmBridge> MediaDrmBridge::CreateWithoutSessionSupport( 434 scoped_ptr<MediaDrmBridge> MediaDrmBridge::CreateWithoutSessionSupport(
387 const std::string& key_system) { 435 const std::string& key_system) {
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
427 DVLOG(1) << __FUNCTION__; 475 DVLOG(1) << __FUNCTION__;
428 476
429 if (session_type != media::MediaKeys::TEMPORARY_SESSION) { 477 if (session_type != media::MediaKeys::TEMPORARY_SESSION) {
430 promise->reject(NOT_SUPPORTED_ERROR, 0, 478 promise->reject(NOT_SUPPORTED_ERROR, 0,
431 "Only the temporary session type is supported."); 479 "Only the temporary session type is supported.");
432 return; 480 return;
433 } 481 }
434 482
435 JNIEnv* env = AttachCurrentThread(); 483 JNIEnv* env = AttachCurrentThread();
436 ScopedJavaLocalRef<jbyteArray> j_init_data; 484 ScopedJavaLocalRef<jbyteArray> j_init_data;
485 ScopedJavaLocalRef<jobjectArray> j_additional_data;
437 // Caller should always use "video/*" content types. 486 // Caller should always use "video/*" content types.
438 DCHECK_EQ(0u, init_data_type.find("video/")); 487 DCHECK_EQ(0u, init_data_type.find("video/"));
439 488
489 /*
490 TODO: find an appropriate place for this to live.
440 // Widevine MediaDrm plugin only accepts the "data" part of the PSSH box as 491 // Widevine MediaDrm plugin only accepts the "data" part of the PSSH box as
441 // the init data when using MP4 container. 492 // the init data when using MP4 container.
442 if (std::equal(scheme_uuid_.begin(), scheme_uuid_.end(), kWidevineUuid) && 493 if (std::equal(scheme_uuid_.begin(), scheme_uuid_.end(), kWidevineUuid) &&
443 init_data_type == "video/mp4") { 494 init_data_type == "video/mp4") {
444 std::vector<uint8> pssh_data; 495 std::vector<uint8> pssh_data;
445 if (!GetPsshData(init_data, init_data_length, scheme_uuid_, &pssh_data)) { 496 if (!GetPsshData(init_data, init_data_length, scheme_uuid_, &pssh_data)) {
446 promise->reject(INVALID_ACCESS_ERROR, 0, "Invalid PSSH data."); 497 promise->reject(INVALID_ACCESS_ERROR, 0, "Invalid PSSH data.");
447 return; 498 return;
448 } 499 }
449 j_init_data = 500 j_init_data =
450 base::android::ToJavaByteArray(env, &pssh_data[0], pssh_data.size()); 501 base::android::ToJavaByteArray(env, &pssh_data[0], pssh_data.size());
451 } else { 502 }
503 */
504
505 if (delegate_) {
506 std::vector<uint8> init_data_from_delegate;
507 std::vector<std::string> additional_data_from_delegate;
508 delegate_->OnCreateSession(init_data_type, init_data, init_data_length,
509 init_data_from_delegate,
510 additional_data_from_delegate);
511 if (!init_data_from_delegate.empty()) {
512 j_init_data = base::android::ToJavaByteArray(
513 env, &init_data_from_delegate[0], init_data_from_delegate.size());
514 }
515 if (!additional_data_from_delegate.empty()) {
516 j_additional_data = base::android::ToJavaArrayOfStrings(
517 env, additional_data_from_delegate);
518 }
519 }
520
521 if (j_init_data.is_null()) {
452 j_init_data = 522 j_init_data =
453 base::android::ToJavaByteArray(env, init_data, init_data_length); 523 base::android::ToJavaByteArray(env, init_data, init_data_length);
454 } 524 }
455 525
456 ScopedJavaLocalRef<jstring> j_mime = 526 ScopedJavaLocalRef<jstring> j_mime =
457 ConvertUTF8ToJavaString(env, init_data_type); 527 ConvertUTF8ToJavaString(env, init_data_type);
458 uint32_t promise_id = cdm_promise_adapter_.SavePromise(promise.Pass()); 528 uint32_t promise_id = cdm_promise_adapter_.SavePromise(promise.Pass());
459 Java_MediaDrmBridge_createSession(env, j_media_drm_.obj(), j_init_data.obj(), 529 Java_MediaDrmBridge_createSession(env, j_media_drm_.obj(), j_init_data.obj(),
460 j_mime.obj(), promise_id); 530 j_mime.obj(), j_additional_data.obj(),
531 promise_id);
461 } 532 }
462 533
463 void MediaDrmBridge::LoadSession( 534 void MediaDrmBridge::LoadSession(
464 SessionType session_type, 535 SessionType session_type,
465 const std::string& session_id, 536 const std::string& session_id,
466 scoped_ptr<media::NewSessionCdmPromise> promise) { 537 scoped_ptr<media::NewSessionCdmPromise> promise) {
467 promise->reject(NOT_SUPPORTED_ERROR, 0, "LoadSession() is not supported."); 538 promise->reject(NOT_SUPPORTED_ERROR, 0, "LoadSession() is not supported.");
468 } 539 }
469 540
470 void MediaDrmBridge::UpdateSession( 541 void MediaDrmBridge::UpdateSession(
(...skipping 174 matching lines...) Expand 10 before | Expand all | Expand 10 after
645 JNIEnv* env = AttachCurrentThread(); 716 JNIEnv* env = AttachCurrentThread();
646 Java_MediaDrmBridge_resetDeviceCredentials(env, j_media_drm_.obj()); 717 Java_MediaDrmBridge_resetDeviceCredentials(env, j_media_drm_.obj());
647 } 718 }
648 719
649 void MediaDrmBridge::OnResetDeviceCredentialsCompleted( 720 void MediaDrmBridge::OnResetDeviceCredentialsCompleted(
650 JNIEnv* env, jobject, bool success) { 721 JNIEnv* env, jobject, bool success) {
651 base::ResetAndReturn(&reset_credentials_cb_).Run(success); 722 base::ResetAndReturn(&reset_credentials_cb_).Run(success);
652 } 723 }
653 724
654 } // namespace media 725 } // namespace media
OLDNEW
« media/base/android/media_drm_bridge.h ('K') | « media/base/android/media_drm_bridge.h ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698