WebKit Bugzilla
Attachment 356177 Details for
Bug 192075
: [GStreamer][EME] CDMInstance should be shipped as a GstContext to the decryptors
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
New Account
|
Log In
Remember
[x]
|
Forgot Password
Login:
[x]
[patch]
Patch for landing
bug-192075-20181130164415.patch (text/plain), 19.61 KB, created by
Xabier RodrÃguez Calvar
on 2018-11-30 07:44:17 PST
(
hide
)
Description:
Patch for landing
Filename:
MIME Type:
Creator:
Xabier RodrÃguez Calvar
Created:
2018-11-30 07:44:17 PST
Size:
19.61 KB
patch
obsolete
>Subversion Revision: 238604 >diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog >index 3d52ea3d8aea6ad143764187907fb9e8c7f063f4..d1a14839739bb50db3eaa080229296069d28f78f 100644 >--- a/Source/WebCore/ChangeLog >+++ b/Source/WebCore/ChangeLog >@@ -1,3 +1,46 @@ >+2018-11-30 Xabier Rodriguez Calvar <calvaris@igalia.com> >+ >+ [GStreamer][EME] CDMInstance should be shipped as a GstContext to the decryptors >+ https://bugs.webkit.org/show_bug.cgi?id=192075 >+ >+ Reviewed by Philippe Normand. >+ >+ So far, we were shipping the CDMInstance in an event to the >+ decryptors and they were requesting it with bus messages when it >+ was not found. Now we ship it with a GstContext that is set to the >+ pipeline and read from the decryptors, which is now always >+ available. >+ >+ As a consequence of changing this flow, the attemptToDecrypt one >+ was affected as well because it was tied to CDMInstance >+ shipment. A workaround was added: when the decryptors send the >+ waitingForKey, an attemptToDecrypt will be performed. A FIXME was >+ added for this. A subconsequence is that >+ attemptToDecryptWithInstance is reworked to rely always in >+ attemptToDecryptWithLocal instance, the former becomes final and >+ the latter virtual. >+ >+ This is a rework, no new tests needed. >+ >+ * platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.cpp: >+ (WebCore::MediaPlayerPrivateGStreamer::handleMessage): >+ * platform/graphics/gstreamer/MediaPlayerPrivateGStreamerBase.cpp: >+ (WebCore::MediaPlayerPrivateGStreamerBase::cdmInstanceAttached): >+ (WebCore::MediaPlayerPrivateGStreamerBase::cdmInstanceDetached): >+ (WebCore::MediaPlayerPrivateGStreamerBase::attemptToDecryptWithLocalInstance): >+ (WebCore::MediaPlayerPrivateGStreamerBase::dispatchCDMInstance): Deleted. >+ * platform/graphics/gstreamer/MediaPlayerPrivateGStreamerBase.h: >+ * platform/graphics/gstreamer/eme/WebKitCommonEncryptionDecryptorGStreamer.cpp: >+ (webkit_media_common_encryption_decrypt_class_init): >+ (webkitMediaCommonEncryptionDecryptTransformInPlace): >+ (webkitMediaCommonEncryptionDecryptIsCDMInstanceAvailable): >+ (webkitMediaCommonEncryptionDecryptSinkEventHandler): >+ (webKitMediaCommonEncryptionDecryptorSetContext): >+ * platform/graphics/gstreamer/mse/MediaPlayerPrivateGStreamerMSE.cpp: >+ (WebCore::MediaPlayerPrivateGStreamerMSE::attemptToDecryptWithLocalInstance): >+ (WebCore::MediaPlayerPrivateGStreamerMSE::attemptToDecryptWithInstance): Deleted. >+ * platform/graphics/gstreamer/mse/MediaPlayerPrivateGStreamerMSE.h: >+ > 2018-11-27 Rob Buis <rbuis@igalia.com> > > Block more ports (427, 548, 6697) >diff --git a/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.cpp b/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.cpp >index 1503b548132fd7d9ce0e0905b2b8ddd4af704bf7..b41da87eba6fea996229592dd208e337e24bd56a 100644 >--- a/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.cpp >+++ b/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.cpp >@@ -1329,12 +1329,13 @@ void MediaPlayerPrivateGStreamer::handleMessage(GstMessage* message) > else if (gst_structure_has_name(structure, "drm-waiting-for-key")) { > GST_DEBUG_OBJECT(pipeline(), "drm-waiting-for-key message from %s", GST_MESSAGE_SRC_NAME(message)); > setWaitingForKey(true); >+ // FIXME: The decryptors should be able to attempt to decrypt after being created and linked in a pipeline but currently they are not and current >+ // architecture does not make this very easy. Fortunately, the arch will change soon and it does not pay off to fix this now with something that could be >+ // more convoluted. In the meantime, force attempt to decrypt when they get blocked. >+ attemptToDecryptWithLocalInstance(); > } else if (gst_structure_has_name(structure, "drm-key-received")) { > GST_DEBUG_OBJECT(pipeline(), "drm-key-received message from %s", GST_MESSAGE_SRC_NAME(message)); > setWaitingForKey(false); >- } else if (gst_structure_has_name(structure, "drm-cdm-instance-needed")) { >- GST_DEBUG_OBJECT(pipeline(), "drm-cdm-instance-needed message from %s", GST_MESSAGE_SRC_NAME(message)); >- dispatchCDMInstance(); > } > #endif > else if (gst_structure_has_name(structure, "http-headers")) { >diff --git a/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamerBase.cpp b/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamerBase.cpp >index 471b93395384b20c14dac9d596bd828a05416d2b..897f292025dd4aac265dd9bddf2b778386cb9b61 100644 >--- a/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamerBase.cpp >+++ b/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamerBase.cpp >@@ -1197,23 +1197,48 @@ void MediaPlayerPrivateGStreamerBase::initializationDataEncountered(InitData&& i > > void MediaPlayerPrivateGStreamerBase::cdmInstanceAttached(CDMInstance& instance) > { >- if (m_cdmInstance != &instance) { >- m_cdmInstance = &instance; >- GST_DEBUG_OBJECT(pipeline(), "CDM instance %p set", m_cdmInstance.get()); >- m_protectionCondition.notifyAll(); >+ ASSERT(isMainThread()); >+ >+ if (m_cdmInstance == &instance) >+ return; >+ >+ if (!m_pipeline) { >+ GST_ERROR("no pipeline yet"); >+ ASSERT_NOT_REACHED(); >+ return; > } >+ >+ m_cdmInstance = &instance; >+ >+ GRefPtr<GstContext> context = adoptGRef(gst_context_new("drm-cdm-instance", FALSE)); >+ GstStructure* contextStructure = gst_context_writable_structure(context.get()); >+ gst_structure_set(contextStructure, "cdm-instance", G_TYPE_POINTER, m_cdmInstance.get(), nullptr); >+ gst_element_set_context(GST_ELEMENT(m_pipeline.get()), context.get()); >+ >+ GST_DEBUG_OBJECT(m_pipeline.get(), "CDM instance %p dispatched as context", m_cdmInstance.get()); >+ >+ m_protectionCondition.notifyAll(); > } > > void MediaPlayerPrivateGStreamerBase::cdmInstanceDetached(CDMInstance& instance) > { >-#ifdef NDEBUG >- UNUSED_PARAM(instance); >-#endif >- if (m_cdmInstance == &instance) { >- GST_DEBUG_OBJECT(pipeline(), "detaching CDM instance %p", m_cdmInstance.get()); >- m_cdmInstance = nullptr; >- m_protectionCondition.notifyAll(); >+ ASSERT(isMainThread()); >+ >+ if (m_cdmInstance != &instance) { >+ GST_WARNING("passed CDMInstance %p is different from stored one %p", &instance, m_cdmInstance.get()); >+ ASSERT_NOT_REACHED(); >+ return; > } >+ >+ ASSERT(m_pipeline); >+ >+ GST_DEBUG_OBJECT(m_pipeline.get(), "detaching CDM instance %p, setting empty context", m_cdmInstance.get()); >+ m_cdmInstance = nullptr; >+ >+ GRefPtr<GstContext> context = adoptGRef(gst_context_new("drm-cdm-instance", FALSE)); >+ gst_element_set_context(GST_ELEMENT(m_pipeline.get()), context.get()); >+ >+ m_protectionCondition.notifyAll(); > } > > void MediaPlayerPrivateGStreamerBase::attemptToDecryptWithInstance(CDMInstance& instance) >@@ -1225,8 +1250,7 @@ void MediaPlayerPrivateGStreamerBase::attemptToDecryptWithInstance(CDMInstance& > > void MediaPlayerPrivateGStreamerBase::attemptToDecryptWithLocalInstance() > { >- bool eventHandled = gst_element_send_event(pipeline(), gst_event_new_custom(GST_EVENT_CUSTOM_DOWNSTREAM_OOB, >- gst_structure_new("attempt-to-decrypt", "cdm-instance", G_TYPE_POINTER, m_cdmInstance.get(), nullptr))); >+ bool eventHandled = gst_element_send_event(pipeline(), gst_event_new_custom(GST_EVENT_CUSTOM_DOWNSTREAM_OOB, gst_structure_new_empty("attempt-to-decrypt"))); > GST_DEBUG("attempting to decrypt, event handled %s", boolForPrinting(eventHandled)); > } > >@@ -1237,13 +1261,6 @@ void MediaPlayerPrivateGStreamerBase::dispatchDecryptionKey(GstBuffer* buffer) > GST_TRACE("emitted decryption cipher key on pipeline, event handled %s", boolForPrinting(eventHandled)); > } > >-void MediaPlayerPrivateGStreamerBase::dispatchCDMInstance() >-{ >- // This function dispatches the CDMInstance in GStreamer playback pipeline. >- if (m_cdmInstance) >- m_player->attemptToDecryptWithInstance(const_cast<CDMInstance&>(*m_cdmInstance.get())); >-} >- > void MediaPlayerPrivateGStreamerBase::handleProtectionEvent(GstEvent* event) > { > if (m_handledProtectionEvents.contains(GST_EVENT_SEQNUM(event))) { >diff --git a/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamerBase.h b/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamerBase.h >index e3e3969c9906d47f41c621d8d779fdb8d3e35afb..42381d2f77b8c14c19d07aa0fc6d7f859c69faaa 100644 >--- a/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamerBase.h >+++ b/Source/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamerBase.h >@@ -150,9 +150,8 @@ public: > void cdmInstanceDetached(CDMInstance&) override; > void dispatchDecryptionKey(GstBuffer*); > void handleProtectionEvent(GstEvent*); >- void attemptToDecryptWithLocalInstance(); >- void attemptToDecryptWithInstance(CDMInstance&) override; >- void dispatchCDMInstance(); >+ virtual void attemptToDecryptWithLocalInstance(); >+ void attemptToDecryptWithInstance(CDMInstance&) final; > void initializationDataEncountered(InitData&&); > void setWaitingForKey(bool); > bool waitingForKey() const override; >diff --git a/Source/WebCore/platform/graphics/gstreamer/eme/WebKitCommonEncryptionDecryptorGStreamer.cpp b/Source/WebCore/platform/graphics/gstreamer/eme/WebKitCommonEncryptionDecryptorGStreamer.cpp >index 8bff38c039bb9f110a855cf181ce7ce7c718d65a..17fd48cc9591e814e6a39b4725f08f90980e5f61 100644 >--- a/Source/WebCore/platform/graphics/gstreamer/eme/WebKitCommonEncryptionDecryptorGStreamer.cpp >+++ b/Source/WebCore/platform/graphics/gstreamer/eme/WebKitCommonEncryptionDecryptorGStreamer.cpp >@@ -31,10 +31,12 @@ > #include <wtf/PrintStream.h> > #include <wtf/RunLoop.h> > >+using WebCore::CDMInstance; >+ > #define WEBKIT_MEDIA_CENC_DECRYPT_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE((obj), WEBKIT_TYPE_MEDIA_CENC_DECRYPT, WebKitMediaCommonEncryptionDecryptPrivate)) > struct _WebKitMediaCommonEncryptionDecryptPrivate { > GRefPtr<GstEvent> protectionEvent; >- RefPtr<WebCore::CDMInstance> cdmInstance; >+ RefPtr<CDMInstance> cdmInstance; > bool keyReceived; > bool waitingForKey { false }; > Lock mutex; >@@ -42,12 +44,13 @@ struct _WebKitMediaCommonEncryptionDecryptPrivate { > }; > > static GstStateChangeReturn webKitMediaCommonEncryptionDecryptorChangeState(GstElement*, GstStateChange transition); >+static void webKitMediaCommonEncryptionDecryptorSetContext(GstElement*, GstContext*); > static void webKitMediaCommonEncryptionDecryptorFinalize(GObject*); > static GstCaps* webkitMediaCommonEncryptionDecryptTransformCaps(GstBaseTransform*, GstPadDirection, GstCaps*, GstCaps*); > static GstFlowReturn webkitMediaCommonEncryptionDecryptTransformInPlace(GstBaseTransform*, GstBuffer*); > static gboolean webkitMediaCommonEncryptionDecryptSinkEventHandler(GstBaseTransform*, GstEvent*); > static gboolean webkitMediaCommonEncryptionDecryptorQueryHandler(GstBaseTransform*, GstPadDirection, GstQuery*); >- >+static bool webkitMediaCommonEncryptionDecryptIsCDMInstanceAvailable(WebKitMediaCommonEncryptionDecrypt*); > > GST_DEBUG_CATEGORY_STATIC(webkit_media_common_encryption_decrypt_debug_category); > #define GST_CAT_DEFAULT webkit_media_common_encryption_decrypt_debug_category >@@ -65,6 +68,7 @@ static void webkit_media_common_encryption_decrypt_class_init(WebKitMediaCommonE > > GstElementClass* elementClass = GST_ELEMENT_CLASS(klass); > elementClass->change_state = GST_DEBUG_FUNCPTR(webKitMediaCommonEncryptionDecryptorChangeState); >+ elementClass->set_context = GST_DEBUG_FUNCPTR(webKitMediaCommonEncryptionDecryptorSetContext); > > GstBaseTransformClass* baseTransformClass = GST_BASE_TRANSFORM_CLASS(klass); > baseTransformClass->transform_ip = GST_DEBUG_FUNCPTR(webkitMediaCommonEncryptionDecryptTransformInPlace); >@@ -209,7 +213,6 @@ static GstFlowReturn webkitMediaCommonEncryptionDecryptTransformInPlace(GstBaseT > // Send "drm-cdm-instance-needed" message to the player to resend the CDMInstance if available and inform we are waiting for key. > priv->waitingForKey = true; > gst_element_post_message(GST_ELEMENT(self), gst_message_new_element(GST_OBJECT(self), gst_structure_new_empty("drm-waiting-for-key"))); >- gst_element_post_message(GST_ELEMENT(self), gst_message_new_element(GST_OBJECT(self), gst_structure_new_empty("drm-cdm-instance-needed"))); > > priv->condition.waitFor(priv->mutex, Seconds(5), [priv] { > return priv->keyReceived; >@@ -299,6 +302,32 @@ static GstFlowReturn webkitMediaCommonEncryptionDecryptTransformInPlace(GstBaseT > return GST_FLOW_OK; > } > >+static bool webkitMediaCommonEncryptionDecryptIsCDMInstanceAvailable(WebKitMediaCommonEncryptionDecrypt* self) >+{ >+ WebKitMediaCommonEncryptionDecryptPrivate* priv = WEBKIT_MEDIA_CENC_DECRYPT_GET_PRIVATE(self); >+ >+ ASSERT(priv->mutex.isLocked()); >+ >+ if (!priv->cdmInstance) { >+ GRefPtr<GstContext> context = adoptGRef(gst_element_get_context(GST_ELEMENT(self), "drm-cdm-instance")); >+ // According to the GStreamer documentation, if we can't find the context, we should run a downstream query, then an upstream one and then send a bus >+ // message. In this case that does not make a lot of sense since only the app (player) answers it, meaning that no query is going to solve it. A message >+ // could be helpful but the player sets the context as soon as it gets the CDMInstance and if it does not have it, we have no way of asking for one as it is >+ // something provided by crossplatform code. This means that we won't be able to answer the bus request in any way either. Summing up, neither queries nor bus >+ // requests are useful here. >+ if (context) { >+ const GValue* value = gst_structure_get_value(gst_context_get_structure(context.get()), "cdm-instance"); >+ priv->cdmInstance = value ? reinterpret_cast<CDMInstance*>(g_value_get_pointer(value)) : nullptr; >+ if (priv->cdmInstance) >+ GST_DEBUG_OBJECT(self, "received new CDMInstance %p", priv->cdmInstance.get()); >+ else >+ GST_TRACE_OBJECT(self, "former instance was detached"); >+ } >+ } >+ >+ GST_TRACE_OBJECT(self, "CDMInstance available %s", boolForPrinting(priv->cdmInstance.get())); >+ return priv->cdmInstance; >+} > > static gboolean webkitMediaCommonEncryptionDecryptSinkEventHandler(GstBaseTransform* trans, GstEvent* event) > { >@@ -307,7 +336,6 @@ static gboolean webkitMediaCommonEncryptionDecryptSinkEventHandler(GstBaseTransf > WebKitMediaCommonEncryptionDecryptClass* klass = WEBKIT_MEDIA_CENC_DECRYPT_GET_CLASS(self); > gboolean result = FALSE; > >- > switch (GST_EVENT_TYPE(event)) { > case GST_EVENT_CUSTOM_DOWNSTREAM_OOB: { > // FIXME: https://bugs.webkit.org/show_bug.cgi?id=191355 >@@ -316,14 +344,12 @@ static gboolean webkitMediaCommonEncryptionDecryptSinkEventHandler(GstBaseTransf > // preferred system ID context is set, any future protection > // events will not be handled by the demuxer, so the must be > // handled in here. >- const GstStructure* structure = gst_event_get_structure(event); >- gst_structure_get(structure, "cdm-instance", G_TYPE_POINTER, &priv->cdmInstance, nullptr); >- if (!priv->cdmInstance) { >- GST_ERROR_OBJECT(self, "No CDM instance received"); >+ LockHolder locker(priv->mutex); >+ if (!webkitMediaCommonEncryptionDecryptIsCDMInstanceAvailable(self)) { >+ GST_ERROR_OBJECT(self, "No CDM instance available"); > result = FALSE; > break; > } >- GST_DEBUG_OBJECT(self, "received a cdm instance %p", priv->cdmInstance.get()); > > if (klass->handleKeyResponse(self, event)) { > GST_DEBUG_OBJECT(self, "key received"); >@@ -374,4 +400,20 @@ static GstStateChangeReturn webKitMediaCommonEncryptionDecryptorChangeState(GstE > return result; > } > >+static void webKitMediaCommonEncryptionDecryptorSetContext(GstElement* element, GstContext* context) >+{ >+ WebKitMediaCommonEncryptionDecrypt* self = WEBKIT_MEDIA_CENC_DECRYPT(element); >+ WebKitMediaCommonEncryptionDecryptPrivate* priv = WEBKIT_MEDIA_CENC_DECRYPT_GET_PRIVATE(self); >+ >+ if (gst_context_has_context_type(context, "drm-cdm-instance")) { >+ const GValue* value = gst_structure_get_value(gst_context_get_structure(context), "cdm-instance"); >+ LockHolder locker(priv->mutex); >+ priv->cdmInstance = value ? reinterpret_cast<CDMInstance*>(g_value_get_pointer(value)) : nullptr; >+ GST_DEBUG_OBJECT(self, "received new CDMInstance %p", priv->cdmInstance.get()); >+ return; >+ } >+ >+ GST_ELEMENT_CLASS(parent_class)->set_context(element, context); >+} >+ > #endif // ENABLE(ENCRYPTED_MEDIA) && USE(GSTREAMER) >diff --git a/Source/WebCore/platform/graphics/gstreamer/mse/MediaPlayerPrivateGStreamerMSE.cpp b/Source/WebCore/platform/graphics/gstreamer/mse/MediaPlayerPrivateGStreamerMSE.cpp >index b86b46836759552525fc9aab8dd63a9758ad460f..bb93ad7288f4f93fbf4589487789791f62ca4c2e 100644 >--- a/Source/WebCore/platform/graphics/gstreamer/mse/MediaPlayerPrivateGStreamerMSE.cpp >+++ b/Source/WebCore/platform/graphics/gstreamer/mse/MediaPlayerPrivateGStreamerMSE.cpp >@@ -909,11 +909,11 @@ MediaTime MediaPlayerPrivateGStreamerMSE::maxMediaTimeSeekable() const > } > > #if ENABLE(ENCRYPTED_MEDIA) >-void MediaPlayerPrivateGStreamerMSE::attemptToDecryptWithInstance(CDMInstance& instance) >+void MediaPlayerPrivateGStreamerMSE::attemptToDecryptWithLocalInstance() > { >- if (is<CDMInstanceClearKey>(instance)) { >- auto& ckInstance = downcast<CDMInstanceClearKey>(instance); >- if (ckInstance.keys().isEmpty()) >+ if (is<CDMInstanceClearKey>(*m_cdmInstance)) { >+ auto& clearkeyCDMInstance = downcast<CDMInstanceClearKey>(*m_cdmInstance); >+ if (clearkeyCDMInstance.keys().isEmpty()) > return; > > GValue keyIDList = G_VALUE_INIT, keyValueList = G_VALUE_INIT; >@@ -930,7 +930,7 @@ void MediaPlayerPrivateGStreamerMSE::attemptToDecryptWithInstance(CDMInstance& i > gst_value_list_append_and_take_value(valueList, bufferValue); > }; > >- for (auto& key : ckInstance.keys()) { >+ for (auto& key : clearkeyCDMInstance.keys()) { > appendBuffer(&keyIDList, *key.keyIDData); > appendBuffer(&keyValueList, *key.keyValueData); > } >@@ -938,7 +938,6 @@ void MediaPlayerPrivateGStreamerMSE::attemptToDecryptWithInstance(CDMInstance& i > GUniquePtr<GstStructure> structure(gst_structure_new_empty("drm-cipher-clearkey")); > gst_structure_set_value(structure.get(), "key-ids", &keyIDList); > gst_structure_set_value(structure.get(), "key-values", &keyValueList); >- gst_structure_set(structure.get(), "cdm-instance", G_TYPE_POINTER, &instance, nullptr); > > gst_element_send_event(m_playbackPipeline->pipeline(), gst_event_new_custom(GST_EVENT_CUSTOM_DOWNSTREAM_OOB, structure.release())); > } >diff --git a/Source/WebCore/platform/graphics/gstreamer/mse/MediaPlayerPrivateGStreamerMSE.h b/Source/WebCore/platform/graphics/gstreamer/mse/MediaPlayerPrivateGStreamerMSE.h >index 2257e9e6830acb18c6f08f7186bf7758b72735d7..5b5682f64b5d35494e839b7a3944ba7e409efc18 100644 >--- a/Source/WebCore/platform/graphics/gstreamer/mse/MediaPlayerPrivateGStreamerMSE.h >+++ b/Source/WebCore/platform/graphics/gstreamer/mse/MediaPlayerPrivateGStreamerMSE.h >@@ -86,7 +86,7 @@ public: > static bool supportsAllCodecs(const Vector<String>& codecs); > > #if ENABLE(ENCRYPTED_MEDIA) >- void attemptToDecryptWithInstance(CDMInstance&) final; >+ void attemptToDecryptWithLocalInstance() final; > #endif > > private:
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Formatted Diff
|
Diff
Attachments on
bug 192075
:
355860
|
356019
|
356023
| 356177