зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
This commit is contained in:
Коммит
de0be877fa
|
@ -61,42 +61,40 @@ class DecodedStreamGraphListener {
|
|||
TrackID aVideoTrackID,
|
||||
MozPromiseHolder<DecodedStream::EndedPromise>&& aVideoEndedHolder,
|
||||
AbstractThread* aMainThread)
|
||||
: mMutex("DecodedStreamGraphListener::mMutex"),
|
||||
mAudioTrackListener(IsTrackIDExplicit(aAudioTrackID)
|
||||
: mAudioTrackListener(IsTrackIDExplicit(aAudioTrackID)
|
||||
? MakeRefPtr<DecodedStreamTrackListener>(
|
||||
this, aStream, aAudioTrackID)
|
||||
: nullptr),
|
||||
mAudioTrackID(aAudioTrackID),
|
||||
mAudioEndedHolder(std::move(aAudioEndedHolder)),
|
||||
mVideoTrackListener(IsTrackIDExplicit(aVideoTrackID)
|
||||
? MakeRefPtr<DecodedStreamTrackListener>(
|
||||
this, aStream, aVideoTrackID)
|
||||
: nullptr),
|
||||
mAudioTrackID(aAudioTrackID),
|
||||
mAudioEndedHolder(std::move(aAudioEndedHolder)),
|
||||
mVideoTrackID(aVideoTrackID),
|
||||
mVideoEndedHolder(std::move(aVideoEndedHolder)),
|
||||
mStream(aStream),
|
||||
mAbstractMainThread(aMainThread) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
if (mAudioTrackListener) {
|
||||
aStream->AddTrackListener(mAudioTrackListener, mAudioTrackID);
|
||||
mStream->AddTrackListener(mAudioTrackListener, mAudioTrackID);
|
||||
} else {
|
||||
mAudioEndedHolder.ResolveIfExists(true, __func__);
|
||||
}
|
||||
|
||||
if (mVideoTrackListener) {
|
||||
aStream->AddTrackListener(mVideoTrackListener, mVideoTrackID);
|
||||
mStream->AddTrackListener(mVideoTrackListener, mVideoTrackID);
|
||||
} else {
|
||||
mVideoEndedHolder.ResolveIfExists(true, __func__);
|
||||
}
|
||||
}
|
||||
|
||||
void NotifyOutput(const RefPtr<SourceMediaStream>& aStream, TrackID aTrackID,
|
||||
StreamTime aCurrentTrackTime) {
|
||||
void NotifyOutput(TrackID aTrackID, StreamTime aCurrentTrackTime) {
|
||||
if (aTrackID != mAudioTrackID && mAudioTrackID != TRACK_NONE) {
|
||||
// Only audio playout drives the clock forward, if present.
|
||||
return;
|
||||
}
|
||||
if (aStream) {
|
||||
mOnOutput.Notify(aStream->StreamTimeToMicroseconds(aCurrentTrackTime));
|
||||
}
|
||||
mOnOutput.Notify(mStream->StreamTimeToMicroseconds(aCurrentTrackTime));
|
||||
}
|
||||
|
||||
TrackID AudioTrackID() const { return mAudioTrackID; }
|
||||
|
@ -115,16 +113,21 @@ class DecodedStreamGraphListener {
|
|||
}
|
||||
|
||||
void Forget() {
|
||||
RefPtr<DecodedStreamGraphListener> self = this;
|
||||
mAbstractMainThread->Dispatch(
|
||||
NS_NewRunnableFunction("DecodedStreamGraphListener::Forget", [self]() {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
self->mAudioEndedHolder.ResolveIfExists(false, __func__);
|
||||
self->mVideoEndedHolder.ResolveIfExists(false, __func__);
|
||||
}));
|
||||
MutexAutoLock lock(mMutex);
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
if (mAudioTrackListener && !mStream->IsDestroyed()) {
|
||||
mStream->EndTrack(mAudioTrackID);
|
||||
mStream->RemoveTrackListener(mAudioTrackListener, mAudioTrackID);
|
||||
}
|
||||
mAudioTrackListener = nullptr;
|
||||
mAudioEndedHolder.ResolveIfExists(false, __func__);
|
||||
|
||||
if (mVideoTrackListener && !mStream->IsDestroyed()) {
|
||||
mStream->EndTrack(mVideoTrackID);
|
||||
mStream->RemoveTrackListener(mVideoTrackListener, mVideoTrackID);
|
||||
}
|
||||
mVideoTrackListener = nullptr;
|
||||
mVideoEndedHolder.ResolveIfExists(false, __func__);
|
||||
}
|
||||
|
||||
MediaEventSource<int64_t>& OnOutput() { return mOnOutput; }
|
||||
|
@ -137,16 +140,15 @@ class DecodedStreamGraphListener {
|
|||
|
||||
MediaEventProducer<int64_t> mOnOutput;
|
||||
|
||||
Mutex mMutex;
|
||||
// Members below are protected by mMutex.
|
||||
RefPtr<DecodedStreamTrackListener> mAudioTrackListener;
|
||||
RefPtr<DecodedStreamTrackListener> mVideoTrackListener;
|
||||
// Main thread only.
|
||||
RefPtr<DecodedStreamTrackListener> mAudioTrackListener;
|
||||
const TrackID mAudioTrackID;
|
||||
MozPromiseHolder<DecodedStream::EndedPromise> mAudioEndedHolder;
|
||||
RefPtr<DecodedStreamTrackListener> mVideoTrackListener;
|
||||
const TrackID mVideoTrackID;
|
||||
MozPromiseHolder<DecodedStream::EndedPromise> mVideoEndedHolder;
|
||||
|
||||
const RefPtr<SourceMediaStream> mStream;
|
||||
const RefPtr<AbstractThread> mAbstractMainThread;
|
||||
};
|
||||
|
||||
|
@ -157,7 +159,7 @@ DecodedStreamTrackListener::DecodedStreamTrackListener(
|
|||
|
||||
void DecodedStreamTrackListener::NotifyOutput(MediaStreamGraph* aGraph,
|
||||
StreamTime aCurrentTrackTime) {
|
||||
mGraphListener->NotifyOutput(mStream, mTrackID, aCurrentTrackTime);
|
||||
mGraphListener->NotifyOutput(mTrackID, aCurrentTrackTime);
|
||||
}
|
||||
|
||||
void DecodedStreamTrackListener::NotifyEnded() {
|
||||
|
@ -450,7 +452,7 @@ void DecodedStream::Shutdown() {
|
|||
mWatchManager.Shutdown();
|
||||
}
|
||||
|
||||
void DecodedStream::DestroyData(UniquePtr<DecodedStreamData> aData) {
|
||||
void DecodedStream::DestroyData(UniquePtr<DecodedStreamData>&& aData) {
|
||||
AssertOwnerThread();
|
||||
|
||||
if (!aData) {
|
||||
|
@ -459,11 +461,9 @@ void DecodedStream::DestroyData(UniquePtr<DecodedStreamData> aData) {
|
|||
|
||||
mOutputListener.Disconnect();
|
||||
|
||||
DecodedStreamData* data = aData.release();
|
||||
data->Forget();
|
||||
nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction("DecodedStream::DestroyData",
|
||||
[=]() { delete data; });
|
||||
NS_DispatchToMainThread(r.forget());
|
||||
NS_DispatchToMainThread(
|
||||
NS_NewRunnableFunction("DecodedStream::DestroyData",
|
||||
[data = std::move(aData)]() { data->Forget(); }));
|
||||
}
|
||||
|
||||
void DecodedStream::SetPlaying(bool aPlaying) {
|
||||
|
|
|
@ -76,7 +76,7 @@ class DecodedStream : public MediaSink {
|
|||
media::TimeUnit FromMicroseconds(int64_t aTime) {
|
||||
return media::TimeUnit::FromMicroseconds(aTime);
|
||||
}
|
||||
void DestroyData(UniquePtr<DecodedStreamData> aData);
|
||||
void DestroyData(UniquePtr<DecodedStreamData>&& aData);
|
||||
void SendAudio(double aVolume, bool aIsSameOrigin,
|
||||
const PrincipalHandle& aPrincipalHandle);
|
||||
void SendVideo(bool aIsSameOrigin, const PrincipalHandle& aPrincipalHandle);
|
||||
|
|
|
@ -767,6 +767,9 @@ skip-if = toolkit == 'android' # bug 1306916, bug 1329566, android(bug 1232305)
|
|||
[test_bug1248229.html]
|
||||
skip-if = android_version == '17' # bug 1306917, 1323778, android(bug 1232305)
|
||||
tags=capturestream
|
||||
[test_bug1512958.html]
|
||||
skip-if = toolkit == 'android' # android(bug 1232305)
|
||||
tags=msg capturestream
|
||||
[test_can_play_type.html]
|
||||
skip-if = (android_version == '23' && debug) || (android_version == '25' && debug) # android(bug 1232305)
|
||||
[test_can_play_type_mpeg.html]
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test that pausing and resuming a captured media element with audio doesn't stall</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<script type="text/javascript" src="manifest.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<audio id="a"></audio>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
function dumpEvent({target, type}) {
|
||||
info(`${target.name} GOT EVENT ${type} currentTime=${target.currentTime} ` +
|
||||
`paused=${target.paused} ended=${target.ended} ` +
|
||||
`readyState=${target.readyState}`);
|
||||
}
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const a = document.getElementById('a');
|
||||
|
||||
const events = ["timeupdate", "seeking", "seeked", "ended", "playing", "pause"];
|
||||
for (let ev of events) {
|
||||
a.addEventListener(ev, dumpEvent);
|
||||
}
|
||||
|
||||
(async _ => {
|
||||
try {
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SimpleTest.requestFlakyTimeout("Timeouts for shortcutting test-timeout");
|
||||
|
||||
const test = getPlayableAudio(gTrackTests.filter(t => t.duration > 2));
|
||||
if (!test) {
|
||||
todo(false, "No playable audio");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start playing and capture
|
||||
a.src = test.name;
|
||||
a.name = test.name;
|
||||
const ac = new AudioContext();
|
||||
const src = ac.createMediaElementSource(a);
|
||||
a.play();
|
||||
do {
|
||||
await new Promise(r => a.ontimeupdate = r);
|
||||
} while(a.currentTime == 0)
|
||||
|
||||
// Pause to trigger recreating tracks in DecodedStream
|
||||
a.pause();
|
||||
await new Promise(r => a.onpause = r);
|
||||
|
||||
// Resuming should now work. Bug 1512958 would cause a stall because the
|
||||
// original track wasn't ended and we'd block on it.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1512958#c5
|
||||
a.play();
|
||||
await new Promise(r => a.onplaying = r);
|
||||
a.currentTime = test.duration - 1;
|
||||
await Promise.race([
|
||||
new Promise(res => a.onended = res),
|
||||
wait(30000).then(_ => Promise.reject(new Error("Timeout"))),
|
||||
]);
|
||||
} catch(e) {
|
||||
ok(false, e);
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -11,6 +11,7 @@
|
|||
#include <cstdint>
|
||||
|
||||
#include "mozilla/Attributes.h"
|
||||
#include "Point.h"
|
||||
#include "Rect.h"
|
||||
#include "Types.h"
|
||||
|
||||
|
@ -33,7 +34,7 @@ namespace gfx {
|
|||
* Do not use this class directly. Subclass it, pass that subclass as the
|
||||
* Sub parameter, and only use that subclass.
|
||||
*/
|
||||
template <class T, class Sub, class Rect>
|
||||
template <class T, class Sub, class Point, class Rect>
|
||||
struct BaseRectAbsolute {
|
||||
protected:
|
||||
T left, top, right, bottom;
|
||||
|
@ -238,17 +239,41 @@ struct BaseRectAbsolute {
|
|||
left = static_cast<T>(ceil(double(left) / aXScale));
|
||||
top = static_cast<T>(ceil(double(top) / aYScale));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate this rectangle to be inside aRect. If it doesn't fit inside
|
||||
* aRect then the dimensions that don't fit will be shrunk so that they
|
||||
* do fit. The resulting rect is returned.
|
||||
*/
|
||||
MOZ_MUST_USE Sub MoveInsideAndClamp(const Sub& aRect) const {
|
||||
T newLeft = std::max(aRect.left, left);
|
||||
T newTop = std::max(aRect.top, top);
|
||||
T width = std::min(aRect.Width(), Width());
|
||||
T height = std::min(aRect.Height(), Height());
|
||||
Sub rect(newLeft, newTop, newLeft + width, newTop + height);
|
||||
newLeft = std::min(rect.right, aRect.right) - width;
|
||||
newTop = std::min(rect.bottom, aRect.bottom) - height;
|
||||
rect.MoveBy(newLeft - rect.left, newTop - rect.top);
|
||||
return rect;
|
||||
}
|
||||
|
||||
friend std::ostream& operator<<(
|
||||
std::ostream& stream,
|
||||
const BaseRectAbsolute<T, Sub, Point, Rect>& aRect) {
|
||||
return stream << '(' << aRect.left << ',' << aRect.top << ',' << aRect.right
|
||||
<< ',' << aRect.bottom << ')';
|
||||
}
|
||||
};
|
||||
|
||||
template <class Units>
|
||||
struct IntRectAbsoluteTyped
|
||||
: public BaseRectAbsolute<int32_t, IntRectAbsoluteTyped<Units>,
|
||||
IntRectTyped<Units>>,
|
||||
IntPointTyped<Units>, IntRectTyped<Units>>,
|
||||
public Units {
|
||||
static_assert(IsPixel<Units>::value,
|
||||
"'units' must be a coordinate system tag");
|
||||
typedef BaseRectAbsolute<int32_t, IntRectAbsoluteTyped<Units>,
|
||||
IntRectTyped<Units>>
|
||||
IntPointTyped<Units>, IntRectTyped<Units>>
|
||||
Super;
|
||||
typedef IntParam<int32_t> ToInt;
|
||||
|
||||
|
@ -260,11 +285,12 @@ struct IntRectAbsoluteTyped
|
|||
template <class Units>
|
||||
struct RectAbsoluteTyped
|
||||
: public BaseRectAbsolute<Float, RectAbsoluteTyped<Units>,
|
||||
RectTyped<Units>>,
|
||||
PointTyped<Units>, RectTyped<Units>>,
|
||||
public Units {
|
||||
static_assert(IsPixel<Units>::value,
|
||||
"'units' must be a coordinate system tag");
|
||||
typedef BaseRectAbsolute<Float, RectAbsoluteTyped<Units>, RectTyped<Units>>
|
||||
typedef BaseRectAbsolute<Float, RectAbsoluteTyped<Units>, PointTyped<Units>,
|
||||
RectTyped<Units>>
|
||||
Super;
|
||||
|
||||
RectAbsoluteTyped() : Super() {}
|
||||
|
|
|
@ -905,9 +905,6 @@ TransactionId LayerTransactionParent::FlushTransactionId(
|
|||
// frame increases. This is effectively including the RecvUpdate work as
|
||||
// part of the 'compositing' phase for this metric, but it isn't included in
|
||||
// COMPOSITE_TIME, and *is* included in CONTENT_FULL_PAINT_TIME.
|
||||
latencyMs = (aCompositeEnd - mRefreshStartTime).ToMilliseconds();
|
||||
latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
|
||||
fracLatencyNorm = lround(latencyNorm * 100.0);
|
||||
if (fracLatencyNorm < 200) {
|
||||
// Success
|
||||
Telemetry::AccumulateCategorical(
|
||||
|
|
|
@ -1976,10 +1976,6 @@ TransactionId WebRenderBridgeParent::FlushTransactionIdsForEpoch(
|
|||
|
||||
// Record CONTENT_FRAME_TIME_REASON.
|
||||
//
|
||||
// This uses the refresh start time (CONTENT_FRAME_TIME uses the start of
|
||||
// display list building), since that includes layout/style time, and 200
|
||||
// should correlate more closely with missing a vsync.
|
||||
//
|
||||
// Also of note is that when the root WebRenderBridgeParent decides to
|
||||
// skip a composite (due to the Renderer being busy), that won't notify
|
||||
// child WebRenderBridgeParents. That failure will show up as the
|
||||
|
@ -1992,9 +1988,6 @@ TransactionId WebRenderBridgeParent::FlushTransactionIdsForEpoch(
|
|||
// child pipelines contained within a render, after it finishes, but I
|
||||
// can't see how to query what child pipeline would have been rendered,
|
||||
// when we choose to not do it.
|
||||
latencyMs = (aEndTime - transactionId.mRefreshStartTime).ToMilliseconds();
|
||||
latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
|
||||
fracLatencyNorm = lround(latencyNorm * 100.0);
|
||||
if (fracLatencyNorm < 200) {
|
||||
// Success
|
||||
Telemetry::AccumulateCategorical(
|
||||
|
|
|
@ -9,11 +9,15 @@
|
|||
|
||||
#include "mozilla/gfx/RectAbsolute.h"
|
||||
#include "nsCoord.h"
|
||||
#include "nsPoint.h"
|
||||
#include "nsRect.h"
|
||||
|
||||
struct nsRectAbsolute
|
||||
: public mozilla::gfx::BaseRectAbsolute<nscoord, nsRectAbsolute, nsRect> {
|
||||
typedef mozilla::gfx::BaseRectAbsolute<nscoord, nsRectAbsolute, nsRect> Super;
|
||||
: public mozilla::gfx::BaseRectAbsolute<nscoord, nsRectAbsolute, nsPoint,
|
||||
nsRect> {
|
||||
typedef mozilla::gfx::BaseRectAbsolute<nscoord, nsRectAbsolute, nsPoint,
|
||||
nsRect>
|
||||
Super;
|
||||
|
||||
nsRectAbsolute() : Super() {}
|
||||
nsRectAbsolute(nscoord aX1, nscoord aY1, nscoord aX2, nscoord aY2)
|
||||
|
@ -40,13 +44,6 @@ struct nsRectAbsolute
|
|||
return Super::Union(aRect);
|
||||
}
|
||||
|
||||
MOZ_ALWAYS_INLINE void MoveBy(const nsPoint& aPoint) {
|
||||
left += aPoint.x;
|
||||
right += aPoint.x;
|
||||
top += aPoint.y;
|
||||
bottom += aPoint.y;
|
||||
}
|
||||
|
||||
void Inflate(const nsMargin& aMargin) {
|
||||
left -= aMargin.left;
|
||||
top -= aMargin.top;
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<style>
|
||||
/*
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
*/
|
||||
* {
|
||||
position: sticky;
|
||||
padding-right: 1px;
|
||||
left: 2px;
|
||||
right: 163px;
|
||||
display: ruby-base;
|
||||
}
|
||||
</style>
|
|
@ -177,3 +177,4 @@ load 1505426-1.html
|
|||
load 1508811.html
|
||||
load 1508822.html
|
||||
load 1509099.html
|
||||
load 1496194.html
|
||||
|
|
|
@ -599,3 +599,36 @@ TEST(Gfx, gfxRect) {
|
|||
TestSetWH<gfxRect>();
|
||||
TestSwap<gfxRect>();
|
||||
}
|
||||
|
||||
static void TestMoveInsideAndClamp(IntRect aSrc, IntRect aTarget,
|
||||
IntRect aExpected) {
|
||||
// Test the implementation in BaseRect (x/y/width/height representation)
|
||||
IntRect result = aSrc.MoveInsideAndClamp(aTarget);
|
||||
EXPECT_TRUE(result.IsEqualEdges(aExpected))
|
||||
<< "Source " << aSrc << " Target " << aTarget << " Expected " << aExpected
|
||||
<< " Actual " << result;
|
||||
|
||||
// Also test the implementation in RectAbsolute (left/top/right/bottom
|
||||
// representation)
|
||||
IntRectAbsolute absSrc = IntRectAbsolute::FromRect(aSrc);
|
||||
IntRectAbsolute absTarget = IntRectAbsolute::FromRect(aTarget);
|
||||
IntRectAbsolute absExpected = IntRectAbsolute::FromRect(aExpected);
|
||||
|
||||
IntRectAbsolute absResult = absSrc.MoveInsideAndClamp(absTarget);
|
||||
EXPECT_TRUE(absResult.IsEqualEdges(absExpected))
|
||||
<< "AbsSource " << absSrc << " AbsTarget " << absTarget << " AbsExpected "
|
||||
<< absExpected << " AbsActual " << absResult;
|
||||
}
|
||||
|
||||
TEST(Gfx, MoveInsideAndClamp) {
|
||||
TestMoveInsideAndClamp(IntRect(0, 0, 10, 10), IntRect(1, -1, 10, 10),
|
||||
IntRect(1, -1, 10, 10));
|
||||
TestMoveInsideAndClamp(IntRect(0, 0, 10, 10), IntRect(-1, -1, 12, 5),
|
||||
IntRect(0, -1, 10, 5));
|
||||
TestMoveInsideAndClamp(IntRect(0, 0, 10, 10), IntRect(10, 11, 10, 0),
|
||||
IntRect(10, 11, 10, 0));
|
||||
TestMoveInsideAndClamp(IntRect(0, 0, 10, 10), IntRect(-10, -1, 10, 0),
|
||||
IntRect(-10, -1, 10, 0));
|
||||
TestMoveInsideAndClamp(IntRect(0, 0, 0, 0), IntRect(10, -10, 10, 10),
|
||||
IntRect(10, 0, 0, 0));
|
||||
}
|
||||
|
|
|
@ -320,6 +320,12 @@ void StickyScrollContainer::GetScrollRanges(nsIFrame* aFrame,
|
|||
// Note that this doesn't necessarily solve all problems stemming from
|
||||
// comparing pre- and post-collapsing margins (TODO: find a proper solution).
|
||||
*aInner = aInner->Intersect(*aOuter);
|
||||
if (aInner->IsEmpty()) {
|
||||
// This might happen if aInner didn't intersect aOuter at all initially,
|
||||
// in which case aInner is empty and outside aOuter. Make sure it doesn't
|
||||
// extend outside aOuter.
|
||||
*aInner = aInner->MoveInsideAndClamp(*aOuter);
|
||||
}
|
||||
}
|
||||
|
||||
void StickyScrollContainer::PositionContinuations(nsIFrame* aFrame) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
lsan-allowed: [js_pod_malloc, js_pod_calloc, js_pod_realloc, js_arena_calloc,js_pod_arena_calloc, maybe_pod_calloc, pod_calloc, make_zeroed_pod_array, js_arena_malloc]
|
||||
leak-threshold:
|
||||
if webrender: [tab:10000, geckomediaplugin:20000, default:16000]
|
||||
if webrender: [tab:10000, gpu: 10000, geckomediaplugin:20000, default:16000]
|
||||
if os == "mac": [tab:10000, geckomediaplugin:20000, default:2000, rdd:400]
|
||||
[tab:10000, geckomediaplugin:20000, rdd:400]
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче