Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  GMPVideoEncoder.cpp

  Sprache: C
 

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


#include "GMPVideoEncoder.h"

#include "AnnexB.h"
#include "ErrorList.h"
#include "GMPLog.h"
#include "GMPService.h"
#include "GMPUtils.h"
#include "GMPVideoHost.h"
#include "H264.h"
#include "ImageContainer.h"
#include "ImageConversion.h"
#include "mozilla/StaticPrefs_media.h"
#include "nsServiceManagerUtils.h"
#include "prsystem.h"

namespace mozilla {

static GMPVideoCodecMode ToGMPVideoCodecMode(Usage aUsage) {
  switch (aUsage) {
    case Usage::Realtime:
      return kGMPRealtimeVideo;
    case Usage::Record:
    default:
      // ParamValidationExt in OpenH264 rejects all other codec modes besides
      // realtime and screensharing.
      return kGMPScreensharing;
  }
}

static GMPProfile ToGMPProfile(H264_PROFILE aProfile) {
  switch (aProfile) {
    case H264_PROFILE_MAIN:
      return kGMPH264ProfileMain;
    case H264_PROFILE_EXTENDED:
      return kGMPH264ProfileExtended;
    case H264_PROFILE_HIGH:
      return kGMPH264ProfileHigh;
    case H264_PROFILE_UNKNOWN:
    default:
      return kGMPH264ProfileUnknown;
  }
}

static GMPLevel ToGMPLevel(H264_LEVEL aLevel) {
  switch (aLevel) {
    case H264_LEVEL::H264_LEVEL_1:
      return kGMPH264Level1_0;
    case H264_LEVEL::H264_LEVEL_1_1:
      // H264_LEVEL_1_b has the same value as H264_LEVEL_1_1, while
      // kGMPH264Level1_B and kGMPH264Level1_1 differ. Since we can't tell the
      // difference, we just ignore the 1_b case.
      return kGMPH264Level1_1;
    case H264_LEVEL::H264_LEVEL_1_2:
      return kGMPH264Level1_2;
    case H264_LEVEL::H264_LEVEL_1_3:
      return kGMPH264Level1_3;
    case H264_LEVEL::H264_LEVEL_2:
      return kGMPH264Level2_0;
    case H264_LEVEL::H264_LEVEL_2_1:
      return kGMPH264Level2_1;
    case H264_LEVEL::H264_LEVEL_2_2:
      return kGMPH264Level2_2;
    case H264_LEVEL::H264_LEVEL_3:
      return kGMPH264Level3_0;
    case H264_LEVEL::H264_LEVEL_3_1:
      return kGMPH264Level3_1;
    case H264_LEVEL::H264_LEVEL_3_2:
      return kGMPH264Level3_2;
    case H264_LEVEL::H264_LEVEL_4:
      return kGMPH264Level4_0;
    case H264_LEVEL::H264_LEVEL_4_1:
      return kGMPH264Level4_1;
    case H264_LEVEL::H264_LEVEL_4_2:
      return kGMPH264Level4_2;
    case H264_LEVEL::H264_LEVEL_5:
      return kGMPH264Level5_0;
    case H264_LEVEL::H264_LEVEL_5_1:
      return kGMPH264Level5_1;
    case H264_LEVEL::H264_LEVEL_5_2:
      return kGMPH264Level5_2;
    // 6.0 and above isn't supported.
    default:
      return kGMPH264LevelUnknown;
  }
}

RefPtr<MediaDataEncoder::InitPromise> GMPVideoEncoder::Init() {
  MOZ_ASSERT(IsOnGMPThread());
  MOZ_ASSERT(mInitPromise.IsEmpty());

  mMPS = do_GetService("@mozilla.org/gecko-media-plugin-service;1");
  MOZ_ASSERT(mMPS);

  RefPtr<InitPromise> promise(mInitPromise.Ensure(__func__));

  nsTArray<nsCString> tags(1);
  tags.AppendElement("h264"_ns);

  UniquePtr<GetGMPVideoEncoderCallback> callback(new InitDoneCallback(this));
  if (NS_FAILED(mMPS->GetGMPVideoEncoder(nullptr, &tags, ""_ns,
                                         std::move(callback)))) {
    GMP_LOG_ERROR("[{}] GMPVideoEncoder::Init -- failed to request encoder",
                  fmt::ptr(this));
    mInitPromise.Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
  }

  return promise;
}

void GMPVideoEncoder::InitComplete(GMPVideoEncoderProxy* aGMP,
                                   GMPVideoHost* aHost) {
  MOZ_ASSERT(IsOnGMPThread());

  mGMP = aGMP;
  mHost = aHost;

  if (NS_WARN_IF(!mGMP) || NS_WARN_IF(!mHost) ||
      NS_WARN_IF(mInitPromise.IsEmpty())) {
    GMP_LOG_ERROR(
        "[{}] GMPVideoEncoder::InitComplete -- failed to create proxy/host",
        fmt::ptr(this));
    Teardown(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "No proxy/host"),
             __func__);
    return;
  }

  GMPVideoCodec codec{};

  codec.mGMPApiVersion = kGMPVersion36;
  codec.mCodecType = kGMPVideoCodecH264;
  codec.mMode = ToGMPVideoCodecMode(mConfig.mUsage);
  codec.mWidth = mConfig.mSize.width;
  codec.mHeight = mConfig.mSize.height;

  // A bitrate need to be set here, attempt to make an educated guess if none is
  // provided.
  if (mConfig.mBitrate) {
    codec.mStartBitrate = mConfig.mBitrate / 1000;
  } else {
    int32_t longDimension = std::max(mConfig.mSize.width, mConfig.mSize.height);
    if (longDimension < 720) {
      codec.mStartBitrate = 2000;
    } else if (longDimension < 1080) {
      codec.mStartBitrate = 4000;
    } else {
      codec.mStartBitrate = 8000;
    }
  }

  codec.mMinBitrate = mConfig.mMinBitrate / 1000;
  codec.mMaxBitrate = mConfig.mMaxBitrate ? mConfig.mMaxBitrate / 1000
                                          : codec.mStartBitrate * 2;
  codec.mMaxFramerate = mConfig.mFramerate;
  codec.mUseThreadedEncode = StaticPrefs::media_gmp_encoder_multithreaded();
  codec.mLogLevel = GetGMPLibraryLogLevel();

  switch (mConfig.mScalabilityMode) {
    case ScalabilityMode::L1T2:
      codec.mTemporalLayerNum = 2;
      break;
    case ScalabilityMode::L1T3:
      codec.mTemporalLayerNum = 3;
      break;
    default:
      MOZ_FALLTHROUGH_ASSERT("Unhandled scalability mode!");
    case ScalabilityMode::None:
      codec.mTemporalLayerNum = 1;
      break;
  }

  if (mConfig.mCodecSpecific.is<H264Specific>()) {
    const H264Specific& specific = mConfig.mCodecSpecific.as<H264Specific>();
    codec.mProfile = ToGMPProfile(specific.mProfile);
    codec.mLevel = ToGMPLevel(specific.mLevel);
  }

  nsTArray<uint8_t> codecSpecific;
  GMPErr err = mGMP->InitEncode(codec, codecSpecific, this,
                                PR_GetNumberOfProcessors(), 0);
  if (NS_WARN_IF(err != GMPNoErr)) {
    GMP_LOG_ERROR("[{}] GMPVideoEncoder::InitComplete -- failed to init proxy",
                  fmt::ptr(this));
    Teardown(ToMediaResult(err, "InitEncode failed"_ns), __func__);
    return;
  }

  GMP_LOG_DEBUG("[{}] GMPVideoEncoder::InitComplete -- encoder initialized",
                fmt::ptr(this));
  mInitPromise.Resolve(true, __func__);
}

RefPtr<MediaDataEncoder::EncodePromise> GMPVideoEncoder::Encode(
    const MediaData* aSample) {
  MOZ_ASSERT(aSample != nullptr);
  MOZ_ASSERT(IsOnGMPThread());

  if (NS_WARN_IF(!IsInitialized())) {
    return EncodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                          __func__);
  }

  GMPVideoFrame* ftmp = nullptr;
  GMPErr err = mHost->CreateFrame(kGMPI420VideoFrame, &ftmp);
  if (NS_WARN_IF(err != GMPNoErr)) {
    GMP_LOG_ERROR("[{}] GMPVideoEncoder::Encode -- failed to create frame",
                  fmt::ptr(this));
    return EncodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                          __func__);
  }

  GMPUniquePtr<GMPVideoi420Frame> frame(static_cast<GMPVideoi420Frame*>(ftmp));
  const VideoData* sample(aSample->As<const VideoData>());
  const uint64_t timestamp = sample->mTime.ToMicroseconds();

  const gfx::IntSize ySize = mConfig.mSize;
  const gfx::IntSize cbCrSize =
      gfx::ChromaSize(ySize, gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT);
  const int32_t yStride = ySize.width;
  const int32_t cbCrStride = cbCrSize.width;

  GMP_LOG_DEBUG(
      "[{}] GMPVideoEncoder::Encode -- request encode of frame @ {} y {}x{} "
      "stride={} cbCr {}x{} stride={}",
      fmt::ptr(this), timestamp, ySize.width, ySize.height, yStride,
      cbCrSize.width, cbCrSize.height, cbCrStride);

  err = frame->CreateEmptyFrame(ySize.width, ySize.height, yStride, cbCrStride,
                                cbCrStride);
  if (NS_WARN_IF(err != GMPNoErr)) {
    GMP_LOG_ERROR(
        "[{}] GMPVideoEncoder::Encode -- failed to allocate frame data",
        fmt::ptr(this));
    return EncodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                          __func__);
  }

  uint8_t* yDest = frame->Buffer(GMPPlaneType::kGMPYPlane);
  uint8_t* uDest = frame->Buffer(GMPPlaneType::kGMPUPlane);
  uint8_t* vDest = frame->Buffer(GMPPlaneType::kGMPVPlane);

  nsresult rv = ConvertToI420(sample->mImage, yDest, yStride, uDest, cbCrStride,
                              vDest, cbCrStride, ySize);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    GMP_LOG_ERROR("[{}] GMPVideoEncoder::Encode -- failed to convert to I420",
                  fmt::ptr(this));
    return EncodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                          __func__);
  }

  frame->SetTimestamp(timestamp);

  AutoTArray<GMPVideoFrameType, 1> frameType;
  frameType.AppendElement(sample->mKeyframe ? kGMPKeyFrame : kGMPDeltaFrame);

  nsTArray<uint8_t> codecSpecific;
  err = mGMP->Encode(std::move(frame), codecSpecific, frameType);
  if (NS_WARN_IF(err != GMPNoErr)) {
    GMP_LOG_ERROR("[{}] GMPVideoEncoder::Encode -- failed to queue frame",
                  fmt::ptr(this));
    return EncodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                          __func__);
  }

  RefPtr<EncodePromise::Private> promise = new EncodePromise::Private(__func__);
  mPendingEncodes.InsertOrUpdate(timestamp, promise);
  return promise.forget();
}

// TODO(Bug 1984936): For realtime mode, resolve the promise after the first
// sample's result is available, then continue processing remaining samples.
// This allows the caller to keep submitting new samples while the encoder
// handles pending ones.
RefPtr<MediaDataEncoder::EncodePromise> GMPVideoEncoder::Encode(
    nsTArray<RefPtr<MediaData>>&& aSamples) {
  MOZ_ASSERT(!aSamples.IsEmpty());
  MOZ_ASSERT(IsOnGMPThread());

  if (NS_WARN_IF(!IsInitialized())) {
    return EncodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                          __func__);
  }

  RefPtr<EncodePromise> promise = mEncodeBatchPromise.Ensure(__func__);
  EncodeNextSample(std::move(aSamples), EncodedData());
  return promise;
}

RefPtr<MediaDataEncoder::ReconfigurationPromise> GMPVideoEncoder::Reconfigure(
    const RefPtr<const EncoderConfigurationChangeList>& aConfigurationChanges) {
  // General reconfiguration interface not implemented right now
  return MediaDataEncoder::ReconfigurationPromise::CreateAndReject(
      NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
}

RefPtr<MediaDataEncoder::EncodePromise> GMPVideoEncoder::Drain() {
  MOZ_ASSERT(IsOnGMPThread());
  MOZ_ASSERT(mDrainPromise.IsEmpty());

  if (NS_WARN_IF(!IsInitialized())) {
    return EncodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                          __func__);
  }

  if (mPendingEncodes.IsEmpty()) {
    return EncodePromise::CreateAndResolve(EncodedData(), __func__);
  }

  GMP_LOG_DEBUG("[{}] GMPVideoEncoder::Drain -- waiting for queue to clear",
                fmt::ptr(this));
  return mDrainPromise.Ensure(__func__);
}

RefPtr<ShutdownPromise> GMPVideoEncoder::Shutdown() {
  GMP_LOG_DEBUG("[{}] GMPVideoEncoder::Shutdown", fmt::ptr(this));
  MOZ_ASSERT(IsOnGMPThread());

  Teardown(MediaResult(NS_ERROR_DOM_MEDIA_CANCELED, "Shutdown"_ns), __func__);
  return ShutdownPromise::CreateAndResolve(true, __func__);
}

RefPtr<GenericPromise> GMPVideoEncoder::SetBitrate(uint32_t aBitsPerSec) {
  GMP_LOG_DEBUG("[{}] GMPVideoEncoder::SetBitrate -- {}", fmt::ptr(this),
                aBitsPerSec);
  MOZ_ASSERT(IsOnGMPThread());

  if (NS_WARN_IF(!IsInitialized())) {
    return GenericPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                           __func__);
  }

  GMPErr err = mGMP->SetRates(aBitsPerSec / 1000, mConfig.mFramerate);
  if (NS_WARN_IF(err != GMPNoErr)) {
    return GenericPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                           __func__);
  }

  return GenericPromise::CreateAndResolve(true, __func__);
}

void GMPVideoEncoder::Encoded(GMPVideoEncodedFrame* aEncodedFrame,
                              const nsTArray<uint8_t>& aCodecSpecificInfo) {
  MOZ_ASSERT(IsOnGMPThread());
  MOZ_ASSERT(aEncodedFrame);

  uint64_t timestamp = aEncodedFrame->TimeStamp();

  RefPtr<EncodePromise::Private> promise;
  if (!mPendingEncodes.Remove(timestamp, getter_AddRefs(promise))) {
    GMP_LOG_WARNING(
        "[{}] GMPVideoEncoder::Encoded -- no frame matching timestamp {}",
        fmt::ptr(this), timestamp);
    return;
  }

  uint8_t* encodedData = aEncodedFrame->Buffer();
  uint32_t encodedSize = aEncodedFrame->Size();

  if (NS_WARN_IF(encodedSize == 0) || NS_WARN_IF(!encodedData) ||
      NS_WARN_IF(aEncodedFrame->BufferType() != GMP_BufferLength32)) {
    GMP_LOG_ERROR("[{}] GMPVideoEncoder::Encoded -- bad/empty frame",
                  fmt::ptr(this));
    promise->Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
    Teardown(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Bad/empty frame"_ns),
             __func__);
    return;
  }

  // This code was copied from WebrtcGmpVideoEncoder::Encoded in order to
  // massage/correct issues with OpenH264 and WebRTC. This allows us to use the
  // PlatformEncoderModule framework with WebRTC, fallback to this encoder and
  // actually render the video.
  if (NS_WARN_IF(!AdjustOpenH264NALUSequence(aEncodedFrame))) {
    promise->Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
    Teardown(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Bad frame data"_ns),
             __func__);
    return;
  }

  auto output = MakeRefPtr<MediaRawData>();

  UniquePtr<MediaRawDataWriter> writer(output->CreateWriter());
  if (NS_WARN_IF(!writer->SetSize(encodedSize))) {
    GMP_LOG_ERROR(
        "[{}] GMPVideoEncoder::Encoded -- failed to allocate {} buffer",
        fmt::ptr(this), encodedSize);
    promise->Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
    Teardown(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Init writer failed"_ns),
             __func__);
    return;
  }

  memcpy(writer->Data(), encodedData, encodedSize);

  output->mTime =
      media::TimeUnit::FromMicroseconds(static_cast<int64_t>(timestamp));
  output->mKeyframe = aEncodedFrame->FrameType() == kGMPKeyFrame;

  int32_t maybeTemporalLayerId = aEncodedFrame->GetTemporalLayerId();
  auto temporalLayerId = CheckedUint8(maybeTemporalLayerId);
  if (temporalLayerId.isValid()) {
    output->mTemporalLayerId = Some(temporalLayerId.value());
  }

  GMP_LOG_DEBUG(
      "[{}] GMPVideoEncoder::Encoded -- {}frame @ timestamp {}, temporal layer "
      "{}",
      fmt::ptr(this), output->mKeyframe ? "key" : "", timestamp,
      maybeTemporalLayerId);

  if (mConfig.mCodecSpecific.is<H264Specific>()) {
    const H264Specific& specific = mConfig.mCodecSpecific.as<H264Specific>();
    if (specific.mFormat == H264BitStreamFormat::AVC) {
      const uint8_t kExtraData[] = {
          1 /* version */,
          static_cast<uint8_t>(specific.mProfile),
          0 /* profile compat (0) */,
          static_cast<uint8_t>(specific.mLevel),
          0xfc | 3 /* nal size - 1 */,
          0xe0 /* num SPS (0) */,
          0 /* num PPS (0) */
      };

      auto extraData = MakeRefPtr<MediaByteBuffer>();
      extraData->AppendElements(kExtraData, std::size(kExtraData));

      if (NS_WARN_IF(!AnnexB::ConvertSampleToAVCC(output, extraData))) {
        GMP_LOG_ERROR(
            "[{}] GMPVideoEncoder::Encoded -- failed to convert to AVCC",
            fmt::ptr(this));
        promise->Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
        Teardown(
            MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Convert AVCC failed"_ns),
            __func__);
        return;
      }
    }
  }

  EncodedData encodedDataSet(1);
  encodedDataSet.AppendElement(std::move(output));
  promise->Resolve(std::move(encodedDataSet), __func__);

  if (mPendingEncodes.IsEmpty()) {
    mDrainPromise.ResolveIfExists(EncodedData(), __func__);
  }
}

void GMPVideoEncoder::Dropped(uint64_t aTimestamp) {
  MOZ_ASSERT(IsOnGMPThread());

  RefPtr<EncodePromise::Private> promise;
  if (!mPendingEncodes.Remove(aTimestamp, getter_AddRefs(promise))) {
    GMP_LOG_WARNING(
        "[{}] GMPVideoEncoder::Dropped -- no frame matching timestamp {}",
        fmt::ptr(this), aTimestamp);
    return;
  }

  promise->Reject(NS_ERROR_DOM_MEDIA_DROPPED_BY_ENCODER_ERR, __func__);
}

void GMPVideoEncoder::Teardown(const MediaResult& aResult,
                               StaticString aCallSite) {
  GMP_LOG_DEBUG("[{}] GMPVideoEncoder::Teardown", fmt::ptr(this));
  MOZ_ASSERT(IsOnGMPThread());

  // Ensure we are kept alive at least until we return.
  RefPtr<GMPVideoEncoder> self(this);

  mEncodeBatchPromise.RejectIfExists(aResult, aCallSite);
  mEncodeBatchRequest.DisconnectIfExists();

  PendingEncodePromises pendingEncodes = std::move(mPendingEncodes);
  for (auto i = pendingEncodes.Iter(); !i.Done(); i.Next()) {
    i.Data()->Reject(aResult, aCallSite);
  }

  mInitPromise.RejectIfExists(aResult, aCallSite);
  mDrainPromise.RejectIfExists(aResult, aCallSite);

  if (mGMP) {
    mGMP->Close();
    mGMP = nullptr;
  }

  mHost = nullptr;
}

void GMPVideoEncoder::Error(GMPErr aError) {
  GMP_LOG_ERROR("[{}] GMPVideoEncoder::Error -- GMPErr({})", fmt::ptr(this),
                uint32_t(aError));
  MOZ_ASSERT(IsOnGMPThread());
  Teardown(ToMediaResult(aError, "Error GMP callback"_ns), __func__);
}

void GMPVideoEncoder::Terminated() {
  GMP_LOG_DEBUG("[{}] GMPVideoEncoder::Terminated", fmt::ptr(this));
  MOZ_ASSERT(IsOnGMPThread());
  Teardown(
      MediaResult(NS_ERROR_DOM_MEDIA_ABORT_ERR, "Terminated GMP callback"_ns),
      __func__);
}

void GMPVideoEncoder::EncodeNextSample(
    nsTArray<RefPtr<MediaData>>&& aInputs,
    MediaDataEncoder::EncodedData&& aOutputs) {
  MOZ_ASSERT(IsOnGMPThread());
  MOZ_ASSERT(IsInitialized());
  MOZ_ASSERT(!mEncodeBatchPromise.IsEmpty());
  MOZ_ASSERT(!mEncodeBatchRequest.Exists());

  if (aInputs.IsEmpty()) {
    GMP_LOG_VERBOSE("[{}] All samples processed. Resolving the encode promise",
                    fmt::ptr(this));
    mEncodeBatchPromise.Resolve(std::move(aOutputs), __func__);
    return;
  }

  GMP_LOG_VERBOSE("[{}] Processing next sample out of {} remaining",
                  fmt::ptr(this), aInputs.Length());
  Encode(aInputs[0])
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self = RefPtr{this}, inputs = std::move(aInputs),
           outputs = std::move(aOutputs)](
              EncodePromise::ResolveOrRejectValue&& aValue) mutable {
            self->mEncodeBatchRequest.Complete();
            if (aValue.IsReject() &&
                aValue.RejectValue().Code() !=
                    NS_ERROR_DOM_MEDIA_DROPPED_BY_ENCODER_ERR) {
              auto& error = aValue.RejectValue();
              GMP_LOG_ERROR(
                  "[{}] GMPVideoEncoder::EncodeNextSample -- failed to encode: "
                  "{}",
                  fmt::ptr(self.get()), error.Description().get());
              self->mEncodeBatchPromise.Reject(error, __func__);
              return;
            }
            inputs.RemoveElementAt(0);
            if (aValue.IsResolve()) {
              outputs.AppendElements(aValue.ResolveValue());
            } else {
              GMP_LOG_WARNING(
                  "[{}] GMPVideoEncoder::EncodeNextSample -- dropped by "
                  "encoder: {}. Continuing.",
                  fmt::ptr(self.get()),
                  aValue.RejectValue().Description().get());
            }
            self->EncodeNextSample(std::move(inputs), std::move(outputs));
          })
      ->Track(mEncodeBatchRequest);
}

}  // namespace mozilla

Messung V0.5 in Prozent
C=99 H=96 G=97

¤ Dauer der Verarbeitung: 0.8 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Statistik
#Sources=277311
#Domains=655579