/* 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 "HLSDemuxer.h"

#include <stdint.h>

#include <algorithm>

#include "HLSUtils.h"
#include "MediaCodec.h"
#include "mozilla/java/GeckoAudioInfoWrappers.h"
#include "mozilla/java/GeckoHLSDemuxerWrapperNatives.h"
#include "mozilla/java/GeckoVideoInfoWrappers.h"
#include "nsPrintfCString.h"

namespace mozilla {

static Atomic<uint32_t> sStreamSourceID(0u);

typedef TrackInfo::TrackType TrackType;
using media::TimeInterval;
using media::TimeIntervals;
using media::TimeUnit;

static VideoRotation getVideoInfoRotation(int aRotation) {
  switch (aRotation) {
    case 0:
      return VideoRotation::kDegree_0;
    case 90:
      return VideoRotation::kDegree_90;
    case 180:
      return VideoRotation::kDegree_180;
    case 270:
      return VideoRotation::kDegree_270;
    default:
      return VideoRotation::kDegree_0;
  }
}

static mozilla::StereoMode getStereoMode(int aMode) {
  switch (aMode) {
    case 0:
      return mozilla::StereoMode::MONO;
    case 1:
      return mozilla::StereoMode::TOP_BOTTOM;
    case 2:
      return mozilla::StereoMode::LEFT_RIGHT;
    default:
      return mozilla::StereoMode::MONO;
  }
}

// HLSDemuxerCallbacksSupport is a native implemented callback class for
// Callbacks in GeckoHLSDemuxerWrapper.java.
// The callback functions will be invoked by JAVA-side thread.
// Should dispatch the task to the demuxer's task queue.
// We ensure the callback will never be invoked after
// HLSDemuxerCallbacksSupport::DisposeNative has been called in ~HLSDemuxer.
class HLSDemuxer::HLSDemuxerCallbacksSupport
    : public java::GeckoHLSDemuxerWrapper::Callbacks::Natives<
          HLSDemuxerCallbacksSupport> {
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HLSDemuxerCallbacksSupport)
 public:
  typedef java::GeckoHLSDemuxerWrapper::Callbacks::Natives<
      HLSDemuxerCallbacksSupport>
      NativeCallbacks;
  using NativeCallbacks::AttachNative;
  using NativeCallbacks::DisposeNative;

  explicit HLSDemuxerCallbacksSupport(HLSDemuxer* aDemuxer)
      : mMutex("HLSDemuxerCallbacksSupport"), mDemuxer(aDemuxer) {
    MOZ_ASSERT(mDemuxer);
  }

  void OnInitialized(bool aHasAudio, bool aHasVideo) {
    HLS_DEBUG("HLSDemuxerCallbacksSupport", "OnInitialized");
    MutexAutoLock lock(mMutex);
    if (!mDemuxer) {
      return;
    }
    RefPtr<HLSDemuxerCallbacksSupport> self = this;
    nsresult rv = mDemuxer->GetTaskQueue()->Dispatch(NS_NewRunnableFunction(
        "HLSDemuxer::HLSDemuxerCallbacksSupport::OnInitialized", [=]() {
          MutexAutoLock lock(self->mMutex);
          if (self->mDemuxer) {
            self->mDemuxer->OnInitialized(aHasAudio, aHasVideo);
          }
        }));
    MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
    (void)rv;
  }

  void OnError(int aErrorCode) {
    HLS_DEBUG("HLSDemuxerCallbacksSupport", "Got error({}) from java side",
              aErrorCode);
    MutexAutoLock lock(mMutex);
    if (!mDemuxer) {
      return;
    }
    RefPtr<HLSDemuxerCallbacksSupport> self = this;
    nsresult rv = mDemuxer->GetTaskQueue()->Dispatch(NS_NewRunnableFunction(
        "HLSDemuxer::HLSDemuxerCallbacksSupport::OnError", [=]() {
          MutexAutoLock lock(self->mMutex);
          if (self->mDemuxer) {
            self->mDemuxer->OnError(aErrorCode);
          }
        }));
    MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
    (void)rv;
  }

  void Detach() {
    MutexAutoLock lock(mMutex);
    mDemuxer = nullptr;
  }

  Mutex mMutex MOZ_UNANNOTATED;

 private:
  ~HLSDemuxerCallbacksSupport() {}
  HLSDemuxer* mDemuxer;
};

HLSDemuxer::HLSDemuxer(int aPlayerId)
    : mTaskQueue(TaskQueue::Create(
          GetMediaThreadPool(MediaThreadType::SUPERVISOR), "HLSDemuxer",
          /* aSupportsTailDispatch = */ false)) {
  MOZ_ASSERT(NS_IsMainThread());
  HLSDemuxerCallbacksSupport::Init();
  mJavaCallbacks = java::GeckoHLSDemuxerWrapper::Callbacks::New();
  MOZ_ASSERT(mJavaCallbacks);

  mCallbackSupport = new HLSDemuxerCallbacksSupport(this);
  HLSDemuxerCallbacksSupport::AttachNative(mJavaCallbacks, mCallbackSupport);

  mHLSDemuxerWrapper =
      java::GeckoHLSDemuxerWrapper::Create(aPlayerId, mJavaCallbacks);
  MOZ_ASSERT(mHLSDemuxerWrapper);
}

void HLSDemuxer::OnInitialized(bool aHasAudio, bool aHasVideo) {
  MOZ_ASSERT(OnTaskQueue());

  if (aHasAudio) {
    mAudioDemuxer = new HLSTrackDemuxer(this, TrackInfo::TrackType::kAudioTrack,
                                        MakeUnique<AudioInfo>());
  }
  if (aHasVideo) {
    mVideoDemuxer = new HLSTrackDemuxer(this, TrackInfo::TrackType::kVideoTrack,
                                        MakeUnique<VideoInfo>());
  }

  mInitPromise.ResolveIfExists(NS_OK, __func__);
}

void HLSDemuxer::OnError(int aErrorCode) {
  MOZ_ASSERT(OnTaskQueue());
  mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
}

RefPtr<HLSDemuxer::InitPromise> HLSDemuxer::Init() {
  RefPtr<HLSDemuxer> self = this;
  return InvokeAsync(GetTaskQueue(), __func__, [self]() {
    RefPtr<InitPromise> p = self->mInitPromise.Ensure(__func__);
    return p;
  });
}

void HLSDemuxer::NotifyDataArrived() {
  HLS_DEBUG("HLSDemuxer", "NotifyDataArrived");
}

uint32_t HLSDemuxer::GetNumberTracks(TrackType aType) const {
  switch (aType) {
    case TrackType::kAudioTrack:
      return mHLSDemuxerWrapper->GetNumberOfTracks(TrackType::kAudioTrack);
    case TrackType::kVideoTrack:
      return mHLSDemuxerWrapper->GetNumberOfTracks(TrackType::kVideoTrack);
    default:
      return 0;
  }
}

already_AddRefed<MediaTrackDemuxer> HLSDemuxer::GetTrackDemuxer(
    TrackType aType, uint32_t aTrackNumber) {
  RefPtr<HLSTrackDemuxer> e = nullptr;
  if (aType == TrackInfo::TrackType::kAudioTrack) {
    e = mAudioDemuxer;
  } else {
    e = mVideoDemuxer;
  }
  return e.forget();
}

bool HLSDemuxer::IsSeekable() const {
  return !mHLSDemuxerWrapper->IsLiveStream();
}

UniquePtr<EncryptionInfo> HLSDemuxer::GetCrypto() {
  // TODO: Currently, our HLS implementation doesn't support encrypted content.
  // Return null at this stage.
  return nullptr;
}

TimeUnit HLSDemuxer::GetNextKeyFrameTime() {
  MOZ_ASSERT(mHLSDemuxerWrapper);
  return TimeUnit::FromMicroseconds(mHLSDemuxerWrapper->GetNextKeyFrameTime());
}

bool HLSDemuxer::OnTaskQueue() const { return mTaskQueue->IsCurrentThreadIn(); }

HLSDemuxer::~HLSDemuxer() {
  HLS_DEBUG("HLSDemuxer", "~HLSDemuxer()");
  mCallbackSupport->Detach();
  if (mHLSDemuxerWrapper) {
    mHLSDemuxerWrapper->Destroy();
    mHLSDemuxerWrapper = nullptr;
  }
  if (mJavaCallbacks) {
    HLSDemuxerCallbacksSupport::DisposeNative(mJavaCallbacks);
    mJavaCallbacks = nullptr;
  }
  mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
}

HLSTrackDemuxer::HLSTrackDemuxer(HLSDemuxer* aParent,
                                 TrackInfo::TrackType aType,
                                 UniquePtr<TrackInfo> aTrackInfo)
    : mParent(aParent),
      mType(aType),
      mMutex("HLSTrackDemuxer"),
      mTrackInfo(std::move(aTrackInfo)) {
  // Only support audio and video track currently.
  MOZ_ASSERT(mType == TrackInfo::kVideoTrack ||
             mType == TrackInfo::kAudioTrack);
  UpdateMediaInfo(0);
}

UniquePtr<TrackInfo> HLSTrackDemuxer::GetInfo() const {
  MutexAutoLock lock(mMutex);
  return mTrackInfo->Clone();
}

RefPtr<HLSTrackDemuxer::SeekPromise> HLSTrackDemuxer::Seek(
    const TimeUnit& aTime) {
  MOZ_ASSERT(mParent, "Called after BreackCycle()");
  return InvokeAsync<TimeUnit&&>(mParent->GetTaskQueue(), this, __func__,
                                 &HLSTrackDemuxer::DoSeek, aTime);
}

RefPtr<HLSTrackDemuxer::SeekPromise> HLSTrackDemuxer::DoSeek(
    const TimeUnit& aTime) {
  MOZ_ASSERT(mParent, "Called after BreackCycle()");
  MOZ_ASSERT(mParent->OnTaskQueue());
  mQueuedSample = nullptr;
  int64_t seekTimeUs = aTime.ToMicroseconds();
  bool result = mParent->mHLSDemuxerWrapper->Seek(seekTimeUs);
  if (!result) {
    return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
                                        __func__);
  }
  TimeUnit seekTime = TimeUnit::FromMicroseconds(seekTimeUs);
  return SeekPromise::CreateAndResolve(seekTime, __func__);
}

RefPtr<HLSTrackDemuxer::SamplesPromise> HLSTrackDemuxer::GetSamples(
    int32_t aNumSamples) {
  MOZ_ASSERT(mParent, "Called after BreackCycle()");
  return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
                     &HLSTrackDemuxer::DoGetSamples, aNumSamples);
}

RefPtr<HLSTrackDemuxer::SamplesPromise> HLSTrackDemuxer::DoGetSamples(
    int32_t aNumSamples) {
  MOZ_ASSERT(mParent, "Called after BreackCycle()");
  MOZ_ASSERT(mParent->OnTaskQueue());
  if (!aNumSamples) {
    return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
                                           __func__);
  }
  RefPtr<SamplesHolder> samples = new SamplesHolder;
  if (mQueuedSample) {
    if (mQueuedSample->mEOS) {
      return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
                                             __func__);
    }
    MOZ_ASSERT(mQueuedSample->mKeyframe, "mQueuedSample must be a keyframe");
    samples->AppendSample(std::move(mQueuedSample));
    MOZ_ASSERT(!mQueuedSample);
    aNumSamples--;
  }
  if (aNumSamples == 0) {
    // Return the queued sample.
    return SamplesPromise::CreateAndResolve(samples, __func__);
  }
  mozilla::jni::ObjectArray::LocalRef demuxedSamples =
      (mType == TrackInfo::kAudioTrack)
          ? mParent->mHLSDemuxerWrapper->GetSamples(TrackInfo::kAudioTrack,
                                                    aNumSamples)
          : mParent->mHLSDemuxerWrapper->GetSamples(TrackInfo::kVideoTrack,
                                                    aNumSamples);
  nsTArray<jni::Object::LocalRef> sampleObjectArray(
      demuxedSamples->GetElements());

  if (sampleObjectArray.IsEmpty()) {
    return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
                                           __func__);
  }

  for (auto&& demuxedSample : sampleObjectArray) {
    java::GeckoHLSSample::LocalRef sample(std::move(demuxedSample));
    if (sample->IsEOS()) {
      HLS_DEBUG("HLSTrackDemuxer", "Met BUFFER_FLAG_END_OF_STREAM.");
      if (samples->GetSamples().IsEmpty()) {
        return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
                                               __func__);
      }
      mQueuedSample = new MediaRawData();
      mQueuedSample->mEOS = true;
      break;
    }
    RefPtr<MediaRawData> mrd = ConvertToMediaRawData(sample);
    if (!mrd) {
      return SamplesPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
    }
    if (!mrd->HasValidTime()) {
      return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
                                             __func__);
    }
    samples->AppendSample(std::move(mrd));
  }
  if (mType == TrackInfo::kVideoTrack &&
      (mNextKeyframeTime.isNothing() ||
       samples->GetSamples().LastElement()->mTime >=
           mNextKeyframeTime.value())) {
    // Only need to find NextKeyFrame for Video
    UpdateNextKeyFrameTime();
  }

  return SamplesPromise::CreateAndResolve(samples, __func__);
}

void HLSTrackDemuxer::UpdateMediaInfo(int index) {
  MOZ_ASSERT(mParent->OnTaskQueue());
  MOZ_ASSERT(mParent->mHLSDemuxerWrapper);
  MutexAutoLock lock(mMutex);
  jni::Object::LocalRef infoObj = nullptr;
  if (mType == TrackType::kAudioTrack) {
    infoObj = mParent->mHLSDemuxerWrapper->GetAudioInfo(index);
    if (!infoObj) {
      NS_WARNING("Failed to get audio info from Java wrapper");
      return;
    }
    auto* audioInfo = mTrackInfo->GetAsAudioInfo();
    MOZ_ASSERT(audioInfo != nullptr);
    HLS_DEBUG("HLSTrackDemuxer", "Update audio info ({})", index);
    java::GeckoAudioInfo::LocalRef audioInfoObj(std::move(infoObj));
    audioInfo->mRate = audioInfoObj->Rate();
    audioInfo->mChannels = audioInfoObj->Channels();
    audioInfo->mProfile = audioInfoObj->Profile();
    audioInfo->mBitDepth = audioInfoObj->BitDepth();
    audioInfo->mMimeType =
        NS_ConvertUTF16toUTF8(audioInfoObj->MimeType()->ToString());
    audioInfo->mDuration = TimeUnit::FromMicroseconds(audioInfoObj->Duration());
    jni::ByteArray::LocalRef csdBytes = audioInfoObj->CodecSpecificData();
    if (csdBytes) {
      auto&& csd = csdBytes->GetElements();
      AudioCodecSpecificBinaryBlob blob;
      blob.mBinaryBlob->AppendElements(reinterpret_cast<uint8_t*>(&csd[0]),
                                       csd.Length());
      audioInfo->mCodecSpecificConfig =
          AudioCodecSpecificVariant{std::move(blob)};
    }
  } else {
    infoObj = mParent->mHLSDemuxerWrapper->GetVideoInfo(index);
    if (!infoObj) {
      NS_WARNING("Failed to get video info from Java wrapper");
      return;
    }
    auto* videoInfo = mTrackInfo->GetAsVideoInfo();
    MOZ_ASSERT(videoInfo != nullptr);
    java::GeckoVideoInfo::LocalRef videoInfoObj(std::move(infoObj));
    videoInfo->mStereoMode = getStereoMode(videoInfoObj->StereoMode());
    videoInfo->mRotation = getVideoInfoRotation(videoInfoObj->Rotation());
    videoInfo->mImage.width = videoInfoObj->DisplayWidth();
    videoInfo->mImage.height = videoInfoObj->DisplayHeight();
    videoInfo->mDisplay.width = videoInfoObj->PictureWidth();
    videoInfo->mDisplay.height = videoInfoObj->PictureHeight();
    videoInfo->mMimeType =
        NS_ConvertUTF16toUTF8(videoInfoObj->MimeType()->ToString());
    videoInfo->mDuration = TimeUnit::FromMicroseconds(videoInfoObj->Duration());
    HLS_DEBUG("HLSTrackDemuxer", "Update video info ({}) / I({}x{}) / D({}x{})",
              index, videoInfo->mImage.width, videoInfo->mImage.height,
              videoInfo->mDisplay.width, videoInfo->mDisplay.height);
  }
}

CryptoSample HLSTrackDemuxer::ExtractCryptoSample(
    size_t aSampleSize,
    java::sdk::MediaCodec::CryptoInfo::LocalRef aCryptoInfo) {
  if (!aCryptoInfo) {
    return CryptoSample{};
  }
  // Extract Crypto information
  CryptoSample crypto;
  char const* msg = "";
  do {
    HLS_DEBUG("HLSTrackDemuxer", "Sample has Crypto Info");

    int32_t mode = 0;
    if (NS_FAILED(aCryptoInfo->Mode(&mode))) {
      msg = "Error when extracting encryption mode.";
      break;
    }
    // We currently only handle ctr mode.
    if (mode != java::sdk::MediaCodec::CRYPTO_MODE_AES_CTR) {
      msg = "Error: unexpected encryption mode.";
      break;
    }

    crypto.mCryptoScheme = CryptoScheme::Cenc;

    mozilla::jni::ByteArray::LocalRef ivData;
    if (NS_FAILED(aCryptoInfo->Iv(&ivData))) {
      msg = "Error when extracting encryption IV.";
      break;
    }
    // Data in mIV is uint8_t and jbyte is signed char
    auto&& ivArr = ivData->GetElements();
    crypto.mIV.AppendElements(reinterpret_cast<uint8_t*>(&ivArr[0]),
                              ivArr.Length());
    crypto.mIVSize = ivArr.Length();
    mozilla::jni::ByteArray::LocalRef keyData;
    if (NS_FAILED(aCryptoInfo->Key(&keyData))) {
      msg = "Error when extracting encryption key.";
      break;
    }
    auto&& keyArr = keyData->GetElements();
    // Data in mKeyId is uint8_t and jbyte is signed char
    crypto.mKeyId.AppendElements(reinterpret_cast<uint8_t*>(&keyArr[0]),
                                 keyArr.Length());

    mozilla::jni::IntArray::LocalRef clearData;
    if (NS_FAILED(aCryptoInfo->NumBytesOfClearData(&clearData))) {
      msg = "Error when extracting clear data.";
      break;
    }
    auto&& clearArr = clearData->GetElements();
    // Data in mPlainSizes is uint32_t, NumBytesOfClearData is int32_t
    crypto.mPlainSizes.AppendElements(reinterpret_cast<uint32_t*>(&clearArr[0]),
                                      clearArr.Length());

    mozilla::jni::IntArray::LocalRef encryptedData;
    if (NS_FAILED(aCryptoInfo->NumBytesOfEncryptedData(&encryptedData))) {
      msg = "Error when extracting encrypted data.";
      break;
    }
    auto&& encryptedArr = encryptedData->GetElements();
    // Data in mEncryptedSizes is uint32_t, NumBytesOfEncryptedData is int32_t
    crypto.mEncryptedSizes.AppendElements(
        reinterpret_cast<uint32_t*>(&encryptedArr[0]), encryptedArr.Length());
    int subSamplesNum = 0;
    if (NS_FAILED(aCryptoInfo->NumSubSamples(&subSamplesNum))) {
      msg = "Error when extracting subsamples.";
      break;
    }
    crypto.mPlainSizes[0] -= (aSampleSize - subSamplesNum);

    return crypto;
  } while (false);

  HLS_DEBUG("HLSTrackDemuxer", "{}", msg);
  return CryptoSample{};
}

RefPtr<MediaRawData> HLSTrackDemuxer::ConvertToMediaRawData(
    java::GeckoHLSSample::LocalRef aSample) {
  java::sdk::MediaCodec::BufferInfo::LocalRef info = aSample->Info();
  // Currently extract PTS, Size and Data without Crypto information.
  // Transform java Sample into MediaRawData
  RefPtr<MediaRawData> mrd = new MediaRawData();
  int64_t presentationTimeUs = 0;
  bool ok = NS_SUCCEEDED(info->PresentationTimeUs(&presentationTimeUs));
  mrd->mTime = TimeUnit::FromMicroseconds(presentationTimeUs);
  mrd->mTimecode = TimeUnit::FromMicroseconds(presentationTimeUs);
  mrd->mKeyframe = aSample->IsKeyFrame();
  mrd->mDuration = (mType == TrackInfo::kVideoTrack)
                       ? TimeUnit::FromMicroseconds(aSample->Duration())
                       : TimeUnit::Zero();

  int32_t size = 0;
  ok &= NS_SUCCEEDED(info->Size(&size));
  if (!ok) {
    HLS_DEBUG("HLSTrackDemuxer",
              "Error occurred during extraction from Sample java object.");
    return nullptr;
  }

  // Update A/V stream souce ID & Audio/VideoInfo for MFR.
  auto sampleFormatIndex = aSample->FormatIndex();
  if (mLastFormatIndex != sampleFormatIndex) {
    mLastFormatIndex = sampleFormatIndex;
    UpdateMediaInfo(mLastFormatIndex);
    MutexAutoLock lock(mMutex);
    mrd->mTrackInfo = new TrackInfoSharedPtr(*mTrackInfo, ++sStreamSourceID);
  }

  // Write payload into MediaRawData
  UniquePtr<MediaRawDataWriter> writer(mrd->CreateWriter());
  if (!writer->SetSize(size)) {
    HLS_DEBUG("HLSTrackDemuxer", "Exit failed to allocate media buffer");
    return nullptr;
  }
  jni::ByteBuffer::LocalRef dest =
      jni::ByteBuffer::New(writer->Data(), writer->Size());
  aSample->WriteToByteBuffer(dest);

  writer->mCrypto = ExtractCryptoSample(writer->Size(), aSample->CryptoInfo());
  return mrd;
}

void HLSTrackDemuxer::Reset() {
  MOZ_ASSERT(mParent, "Called after BreackCycle()");
  mQueuedSample = nullptr;
}

void HLSTrackDemuxer::UpdateNextKeyFrameTime() {
  MOZ_ASSERT(mParent, "Called after BreackCycle()");
  TimeUnit nextKeyFrameTime = mParent->GetNextKeyFrameTime();
  if (nextKeyFrameTime != mNextKeyframeTime.refOr(TimeUnit::FromInfinity())) {
    HLS_DEBUG("HLSTrackDemuxer", "Update mNextKeyframeTime to {}",
              nextKeyFrameTime.ToMicroseconds());
    mNextKeyframeTime = Some(nextKeyFrameTime);
  }
}

nsresult HLSTrackDemuxer::GetNextRandomAccessPoint(TimeUnit* aTime) {
  if (mNextKeyframeTime.isNothing()) {
    // There's no next key frame.
    *aTime = TimeUnit::FromInfinity();
  } else {
    *aTime = mNextKeyframeTime.value();
  }
  return NS_OK;
}

RefPtr<HLSTrackDemuxer::SkipAccessPointPromise>
HLSTrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
  return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
                     &HLSTrackDemuxer::DoSkipToNextRandomAccessPoint,
                     aTimeThreshold);
}

RefPtr<HLSTrackDemuxer::SkipAccessPointPromise>
HLSTrackDemuxer::DoSkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
  MOZ_ASSERT(mParent, "Called after BreackCycle()");
  MOZ_ASSERT(mParent->OnTaskQueue());
  mQueuedSample = nullptr;
  uint32_t parsed = 0;
  bool found = false;
  MediaResult result = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
  do {
    mozilla::jni::ObjectArray::LocalRef demuxedSamples =
        mParent->mHLSDemuxerWrapper->GetSamples(mType, 1);
    nsTArray<jni::Object::LocalRef> sampleObjectArray(
        demuxedSamples->GetElements());
    if (sampleObjectArray.IsEmpty()) {
      result = NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA;
      break;
    }
    parsed++;
    java::GeckoHLSSample::LocalRef sample(std::move(sampleObjectArray[0]));
    if (sample->IsEOS()) {
      result = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
      break;
    }
    if (sample->IsKeyFrame()) {
      java::sdk::MediaCodec::BufferInfo::LocalRef info = sample->Info();
      int64_t presentationTimeUs = 0;
      if (NS_SUCCEEDED(info->PresentationTimeUs(&presentationTimeUs)) &&
          TimeUnit::FromMicroseconds(presentationTimeUs) >= aTimeThreshold) {
        RefPtr<MediaRawData> rawData = ConvertToMediaRawData(sample);
        if (!rawData) {
          result = NS_ERROR_OUT_OF_MEMORY;
          break;
        }
        if (!rawData->HasValidTime()) {
          result = NS_ERROR_DOM_MEDIA_DEMUXER_ERR;
          break;
        }
        found = true;
        mQueuedSample = rawData;
        break;
      }
    }
  } while (true);

  if (!found) {
    return SkipAccessPointPromise::CreateAndReject(
        SkipFailureHolder(result, parsed), __func__);
  }
  return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
}

TimeIntervals HLSTrackDemuxer::GetBuffered() {
  MOZ_ASSERT(mParent, "Called after BreackCycle()");
  int64_t bufferedTime = mParent->mHLSDemuxerWrapper->GetBuffered();  // us
  return TimeIntervals(
      TimeInterval(TimeUnit(), TimeUnit::FromMicroseconds(bufferedTime)));
}

void HLSTrackDemuxer::BreakCycles() {
  RefPtr<HLSTrackDemuxer> self = this;
  nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
      "HLSTrackDemuxer::BreakCycles", [self]() { self->mParent = nullptr; });
  nsresult rv = mParent->GetTaskQueue()->Dispatch(task.forget());
  MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
  (void)rv;
}

HLSTrackDemuxer::~HLSTrackDemuxer() {}

}  // namespace mozilla
