summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArtem Dyomin <artem.dyomin@qt.io>2023-03-02 14:09:15 +0100
committerArtem Dyomin <artem.dyomin@qt.io>2023-03-09 05:18:40 +0000
commit7306154c02f32c169b73ff69e6e61d31999f127a (patch)
treed1bc46b014f1c08722c998f586577721721c7fe5
parentad21ea6e6e94a2ad9926408ca60c714ccca37184 (diff)
downloadqtmultimedia-7306154c02f32c169b73ff69e6e61d31999f127a.tar.gz
Implement looping for ffmpeg mediaplayer backend
- Looping of ffmpeg mediaplayer has been added with auto tests - Minor code cleanup (that left from previous implmntations) Task-number: QTBUG-111209 Change-Id: I3839ee866bc2dc571919e41a18b093fb15165293 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Lars Knoll <lars@knoll.priv.no> (cherry picked from commit 46394944487b189962c1ceef8a236b328e8e7736)
-rw-r--r--src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder.cpp8
-rw-r--r--src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder_p.h12
-rw-r--r--src/plugins/multimedia/ffmpeg/qffmpegaudiodecoder.cpp2
-rw-r--r--src/plugins/multimedia/ffmpeg/qffmpegmediaplayer.cpp117
-rw-r--r--src/plugins/multimedia/ffmpeg/qffmpegmediaplayer_p.h18
-rw-r--r--src/plugins/multimedia/ffmpeg/qffmpegplaybackengine.cpp4
-rw-r--r--src/plugins/multimedia/ffmpeg/qffmpegplaybackengine_p.h11
-rw-r--r--tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp126
8 files changed, 224 insertions, 74 deletions
diff --git a/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder.cpp b/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder.cpp
index 1a016acc8..eb4f35b7a 100644
--- a/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder.cpp
+++ b/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder.cpp
@@ -267,6 +267,14 @@ int MediaDataHolder::activeTrack(QPlatformMediaPlayer::TrackType type) const
return type < QPlatformMediaPlayer::NTrackTypes ? m_requestedStreams[type] : -1;
}
+const QList<MediaDataHolder::StreamInfo> &MediaDataHolder::streamInfo(
+ QPlatformMediaPlayer::TrackType trackType) const
+{
+ Q_ASSERT(trackType < QPlatformMediaPlayer::NTrackTypes);
+
+ return m_streamMap[trackType];
+}
+
} // namespace QFFmpeg
QT_END_NAMESPACE
diff --git a/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder_p.h b/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder_p.h
index dc9f4c907..e1b6873a8 100644
--- a/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder_p.h
+++ b/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder_p.h
@@ -52,10 +52,16 @@ public:
static QPlatformMediaPlayer::TrackType trackTypeFromMediaType(int mediaType);
- bool setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber);
-
int activeTrack(QPlatformMediaPlayer::TrackType type) const;
+ const QList<StreamInfo> &streamInfo(QPlatformMediaPlayer::TrackType trackType) const;
+
+ qint64 duration() const { return m_duration; }
+
+ const QMediaMetaData &metaData() const { return m_metaData; }
+
+ bool isSeekable() const { return m_isSeekable; }
+
protected:
std::optional<ContextError> recreateAVFormatContext(const QUrl &media, QIODevice *stream);
@@ -63,6 +69,8 @@ protected:
void updateMetaData();
+ bool setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber);
+
protected:
std::unique_ptr<AVFormatContext, AVFormatContextDeleter> m_context;
bool m_isSeekable = false;
diff --git a/src/plugins/multimedia/ffmpeg/qffmpegaudiodecoder.cpp b/src/plugins/multimedia/ffmpeg/qffmpegaudiodecoder.cpp
index 65e8d4148..c40826383 100644
--- a/src/plugins/multimedia/ffmpeg/qffmpegaudiodecoder.cpp
+++ b/src/plugins/multimedia/ffmpeg/qffmpegaudiodecoder.cpp
@@ -149,7 +149,7 @@ void QFFmpegAudioDecoder::start()
if (!checkNoError())
return;
- durationChanged(m_decoder->m_duration / 1000);
+ durationChanged(m_decoder->duration() / 1000);
setIsDecoding(true);
}
diff --git a/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer.cpp b/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer.cpp
index 0041c9a43..507550ab5 100644
--- a/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer.cpp
+++ b/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer.cpp
@@ -7,7 +7,8 @@
#include "qaudiooutput.h"
#include "qffmpegplaybackengine_p.h"
-
+#include <qiodevice.h>
+#include <qvideosink.h>
#include <qtimer.h>
#include <qloggingcategory.h>
@@ -19,22 +20,22 @@ using namespace QFFmpeg;
QFFmpegMediaPlayer::QFFmpegMediaPlayer(QMediaPlayer *player)
: QPlatformMediaPlayer(player)
{
- positionUpdateTimer.setInterval(50);
- positionUpdateTimer.setTimerType(Qt::PreciseTimer);
- connect(&positionUpdateTimer, &QTimer::timeout, this, &QFFmpegMediaPlayer::updatePosition);
+ m_positionUpdateTimer.setInterval(50);
+ m_positionUpdateTimer.setTimerType(Qt::PreciseTimer);
+ connect(&m_positionUpdateTimer, &QTimer::timeout, this, &QFFmpegMediaPlayer::updatePosition);
}
QFFmpegMediaPlayer::~QFFmpegMediaPlayer() = default;
qint64 QFFmpegMediaPlayer::duration() const
{
- return decoder ? decoder->m_duration / 1000 : 0;
+ return m_playbackEngine ? m_playbackEngine->duration() / 1000 : 0;
}
void QFFmpegMediaPlayer::setPosition(qint64 position)
{
- if (decoder) {
- decoder->seek(position * 1000);
+ if (m_playbackEngine) {
+ m_playbackEngine->seek(position * 1000);
updatePosition();
}
if (state() == QMediaPlayer::StoppedState)
@@ -43,15 +44,24 @@ void QFFmpegMediaPlayer::setPosition(qint64 position)
void QFFmpegMediaPlayer::updatePosition()
{
- positionChanged(decoder ? decoder->currentPosition() / 1000 : 0);
+ positionChanged(m_playbackEngine ? m_playbackEngine->currentPosition() / 1000 : 0);
}
void QFFmpegMediaPlayer::endOfStream()
{
- positionUpdateTimer.stop();
+ // start update timer and report end position anyway
+ m_positionUpdateTimer.stop();
positionChanged(duration());
- stateChanged(QMediaPlayer::StoppedState);
- mediaStatusChanged(QMediaPlayer::EndOfMedia);
+
+ if (doLoop()) {
+ m_playbackEngine->seek(0);
+ positionChanged(0);
+
+ runPlayback();
+ } else {
+ stateChanged(QMediaPlayer::StoppedState);
+ mediaStatusChanged(QMediaPlayer::EndOfMedia);
+ }
}
float QFFmpegMediaPlayer::bufferProgress() const
@@ -74,8 +84,8 @@ void QFFmpegMediaPlayer::setPlaybackRate(qreal rate)
if (m_playbackRate == rate)
return;
m_playbackRate = rate;
- if (decoder)
- decoder->setPlaybackRate(rate);
+ if (m_playbackEngine)
+ m_playbackEngine->setPlaybackRate(rate);
}
QUrl QFFmpegMediaPlayer::media() const
@@ -92,7 +102,7 @@ void QFFmpegMediaPlayer::setMedia(const QUrl &media, QIODevice *stream)
{
m_url = media;
m_device = stream;
- decoder = nullptr;
+ m_playbackEngine = nullptr;
positionChanged(0);
@@ -110,66 +120,76 @@ void QFFmpegMediaPlayer::setMedia(const QUrl &media, QIODevice *stream)
}
mediaStatusChanged(QMediaPlayer::LoadingMedia);
- decoder = std::make_unique<Decoder>();
- connect(decoder.get(), &Decoder::endOfStream, this, &QFFmpegMediaPlayer::endOfStream);
- connect(decoder.get(), &Decoder::errorOccured, this, &QFFmpegMediaPlayer::error);
-
- if (!decoder->setMedia(media, stream)) {
- decoder.reset();
+ m_playbackEngine = std::make_unique<PlaybackEngine>();
+ connect(m_playbackEngine.get(), &PlaybackEngine::endOfStream, this,
+ &QFFmpegMediaPlayer::endOfStream);
+ connect(m_playbackEngine.get(), &PlaybackEngine::errorOccured, this,
+ &QFFmpegMediaPlayer::error);
+
+ if (!m_playbackEngine->setMedia(media, stream)) {
+ m_playbackEngine.reset();
handleIncorrectMedia(QMediaPlayer::InvalidMedia);
return;
}
- decoder->setAudioSink(m_audioOutput);
- decoder->setVideoSink(m_videoSink);
+ m_playbackEngine->setAudioSink(m_audioOutput);
+ m_playbackEngine->setVideoSink(m_videoSink);
durationChanged(duration());
tracksChanged();
metaDataChanged();
- seekableChanged(decoder->isSeekable());
-
- audioAvailableChanged(!decoder->m_streamMap[QPlatformMediaPlayer::AudioStream].isEmpty());
- videoAvailableChanged(!decoder->m_streamMap[QPlatformMediaPlayer::VideoStream].isEmpty());
+ seekableChanged(m_playbackEngine->isSeekable());
+ audioAvailableChanged(
+ !m_playbackEngine->streamInfo(QPlatformMediaPlayer::AudioStream).isEmpty());
+ videoAvailableChanged(
+ !m_playbackEngine->streamInfo(QPlatformMediaPlayer::VideoStream).isEmpty());
QMetaObject::invokeMethod(this, "delayedLoadedStatus", Qt::QueuedConnection);
}
void QFFmpegMediaPlayer::play()
{
- if (!decoder)
+ if (!m_playbackEngine)
return;
if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) {
- decoder->seek(0);
+ m_playbackEngine->seek(0);
positionChanged(0);
+ resetCurrentLoop();
}
- decoder->play();
- positionUpdateTimer.start();
+
+ runPlayback();
+}
+
+void QFFmpegMediaPlayer::runPlayback()
+{
+ m_playbackEngine->play();
+ m_positionUpdateTimer.start();
stateChanged(QMediaPlayer::PlayingState);
mediaStatusChanged(QMediaPlayer::BufferedMedia);
}
void QFFmpegMediaPlayer::pause()
{
- if (!decoder)
+ if (!m_playbackEngine)
return;
if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) {
- decoder->seek(0);
+ m_playbackEngine->seek(0);
positionChanged(0);
}
- decoder->pause();
- positionUpdateTimer.stop();
+ m_playbackEngine->pause();
+ m_positionUpdateTimer.stop();
stateChanged(QMediaPlayer::PausedState);
mediaStatusChanged(QMediaPlayer::BufferedMedia);
}
void QFFmpegMediaPlayer::stop()
{
- if (!decoder)
+ if (!m_playbackEngine)
return;
- decoder->stop();
- positionUpdateTimer.stop();
+ m_playbackEngine->stop();
+ m_positionUpdateTimer.stop();
positionChanged(0);
stateChanged(QMediaPlayer::StoppedState);
mediaStatusChanged(QMediaPlayer::LoadedMedia);
@@ -181,13 +201,13 @@ void QFFmpegMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
return;
m_audioOutput = output;
- if (decoder)
- decoder->setAudioSink(output);
+ if (m_playbackEngine)
+ m_playbackEngine->setAudioSink(output);
}
QMediaMetaData QFFmpegMediaPlayer::metaData() const
{
- return decoder ? decoder->m_metaData : QMediaMetaData{};
+ return m_playbackEngine ? m_playbackEngine->metaData() : QMediaMetaData{};
}
void QFFmpegMediaPlayer::setVideoSink(QVideoSink *sink)
@@ -196,8 +216,8 @@ void QFFmpegMediaPlayer::setVideoSink(QVideoSink *sink)
return;
m_videoSink = sink;
- if (decoder)
- decoder->setVideoSink(sink);
+ if (m_playbackEngine)
+ m_playbackEngine->setVideoSink(sink);
}
QVideoSink *QFFmpegMediaPlayer::videoSink() const
@@ -207,25 +227,26 @@ QVideoSink *QFFmpegMediaPlayer::videoSink() const
int QFFmpegMediaPlayer::trackCount(TrackType type)
{
- return decoder ? decoder->m_streamMap[type].count() : 0;
+ return m_playbackEngine ? m_playbackEngine->streamInfo(type).count() : 0;
}
QMediaMetaData QFFmpegMediaPlayer::trackMetaData(TrackType type, int streamNumber)
{
- if (!decoder || streamNumber < 0 || streamNumber >= decoder->m_streamMap[type].count())
+ if (!m_playbackEngine || streamNumber < 0
+ || streamNumber >= m_playbackEngine->streamInfo(type).count())
return {};
- return decoder->m_streamMap[type].at(streamNumber).metaData;
+ return m_playbackEngine->streamInfo(type).at(streamNumber).metaData;
}
int QFFmpegMediaPlayer::activeTrack(TrackType type)
{
- return decoder ? decoder->activeTrack(type) : -1;
+ return m_playbackEngine ? m_playbackEngine->activeTrack(type) : -1;
}
void QFFmpegMediaPlayer::setActiveTrack(TrackType type, int streamNumber)
{
- if (decoder)
- decoder->setActiveTrack(type, streamNumber);
+ if (m_playbackEngine)
+ m_playbackEngine->setActiveTrack(type, streamNumber);
}
QT_END_NAMESPACE
diff --git a/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer_p.h b/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer_p.h
index 75e9ce5f3..7e9126a22 100644
--- a/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer_p.h
+++ b/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer_p.h
@@ -18,12 +18,12 @@
#include <private/qplatformmediaplayer_p.h>
#include <qmediametadata.h>
#include <qtimer.h>
+#include <qpointer.h>
#include "qffmpeg_p.h"
QT_BEGIN_NAMESPACE
namespace QFFmpeg {
-class Decoder;
class PlaybackEngine;
}
class QPlatformAudioOutput;
@@ -32,7 +32,6 @@ class QFFmpegMediaPlayer : public QObject, public QPlatformMediaPlayer
{
Q_OBJECT
public:
- using Decoder = QFFmpeg::PlaybackEngine;
QFFmpegMediaPlayer(QMediaPlayer *player);
~QFFmpegMediaPlayer();
@@ -55,8 +54,6 @@ public:
void pause() override;
void stop() override;
-// bool streamPlaybackSupported() const { return false; }
-
void setAudioOutput(QPlatformAudioOutput *) override;
QMediaMetaData metaData() const override;
@@ -71,6 +68,9 @@ public:
Q_INVOKABLE void delayedLoadedStatus() { mediaStatusChanged(QMediaPlayer::LoadedMedia); }
+private:
+ void runPlayback();
+
private slots:
void updatePosition();
void endOfStream();
@@ -80,16 +80,16 @@ private slots:
}
private:
- friend class QFFmpeg::Decoder;
+ QTimer m_positionUpdateTimer;
- QTimer positionUpdateTimer;
+ using PlaybackEngine = QFFmpeg::PlaybackEngine;
- std::unique_ptr<Decoder> decoder;
+ std::unique_ptr<PlaybackEngine> m_playbackEngine;
QPlatformAudioOutput *m_audioOutput = nullptr;
- QVideoSink *m_videoSink = nullptr;
+ QPointer<QVideoSink> m_videoSink;
QUrl m_url;
- QIODevice *m_device = nullptr;
+ QPointer<QIODevice> m_device;
float m_playbackRate = 1.;
};
diff --git a/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine.cpp b/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine.cpp
index ca646c5c8..19105499d 100644
--- a/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine.cpp
+++ b/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine.cpp
@@ -425,10 +425,6 @@ void PlaybackEngine::setAudioSink(QAudioOutput *output)
forceUpdate();
}
-bool PlaybackEngine::isSeekable() const {
- return m_isSeekable;
-}
-
qint64 PlaybackEngine::currentPosition() const {
auto pos = std::numeric_limits<qint64>::max();
diff --git a/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine_p.h b/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine_p.h
index 48e939f82..682d9093b 100644
--- a/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine_p.h
+++ b/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine_p.h
@@ -62,7 +62,7 @@ class QFFmpegMediaPlayer;
namespace QFFmpeg
{
-class PlaybackEngine : public QObject, protected MediaDataHolder
+class PlaybackEngine : public QObject, public MediaDataHolder
{
Q_OBJECT
public:
@@ -100,17 +100,8 @@ public:
using MediaDataHolder::activeTrack;
- bool isSeekable() const;
-
qint64 currentPosition() const;
- // To be removed after aligning with Decoder
- using MediaDataHolder::m_currentAVStreamIndex;
- using MediaDataHolder::m_duration;
- using MediaDataHolder::m_metaData;
- using MediaDataHolder::m_requestedStreams;
- using MediaDataHolder::m_streamMap;
-
signals:
void endOfStream();
void errorOccured(int, const QString &);
diff --git a/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp b/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp
index 0786145da..6e1365808 100644
--- a/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp
+++ b/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp
@@ -65,6 +65,8 @@ private slots:
void multipleSeekStressTest();
void playbackRateChanging();
void durationDetectionIssues();
+ void finiteLoops();
+ void infiteLoops();
private:
QUrl selectVideoFile(const QStringList& mediaCandidates);
@@ -1619,6 +1621,130 @@ void tst_QMediaPlayerBackend::durationDetectionIssues()
QCOMPARE(videoTracks.front().value(QMediaMetaData::Duration), QVariant(qint64(400)));
}
+static std::vector<std::pair<qint64, qint64>>
+positionChangingIntervals(const QSignalSpy &positionSpy)
+{
+ std::vector<std::pair<qint64, qint64>> result;
+ for (auto &params : positionSpy) {
+ const auto pos = params.front().value<qint64>();
+
+ if (result.empty() || pos < result.back().second)
+ result.emplace_back(pos, pos);
+ else
+ result.back().second = pos;
+ }
+
+ return result;
+}
+
+void tst_QMediaPlayerBackend::finiteLoops()
+{
+ if (localVideoFile3ColorsWithSound.isEmpty())
+ QSKIP("Video format is not supported");
+
+#ifdef Q_OS_MACOS
+ if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci")
+ QSKIP("The test accidently gets crashed on macOS CI, not reproduced locally. To be "
+ "investigated: QTBUG-111744");
+#endif
+
+ TestVideoSink surface(false);
+ QMediaPlayer player;
+
+ QSignalSpy positionSpy(&player, &QMediaPlayer::positionChanged);
+
+ player.setVideoOutput(&surface);
+
+ QCOMPARE(player.loops(), 1);
+ player.setLoops(3);
+ QCOMPARE(player.loops(), 3);
+
+ player.setSource(localVideoFile3ColorsWithSound);
+ player.setPlaybackRate(5);
+
+ player.play();
+ surface.waitForFrame();
+
+ // check pause doesn't affect looping
+ {
+ QTest::qWait(static_cast<int>(player.duration() * 3
+ * 0.6 /*relative pos*/ / player.playbackRate()));
+ player.pause();
+ player.play();
+ }
+
+ QTRY_COMPARE(player.playbackState(), QMediaPlayer::StoppedState);
+
+ auto intervals = positionChangingIntervals(positionSpy);
+
+ QCOMPARE(intervals.size(), 3);
+ QCOMPARE_GT(intervals[0].first, 0);
+ QCOMPARE(intervals[0].second, player.duration());
+ QCOMPARE(intervals[1].first, 0);
+ QCOMPARE(intervals[1].second, player.duration());
+ QCOMPARE(intervals[2].first, 0);
+ QCOMPARE(intervals[2].second, player.duration());
+
+ QCOMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
+
+ // be sure that counter is reset if repeat the same
+ {
+ positionSpy.clear();
+ player.play();
+ player.setPlaybackRate(10);
+ surface.waitForFrame();
+
+ QTRY_COMPARE(player.playbackState(), QMediaPlayer::StoppedState);
+ QCOMPARE(positionChangingIntervals(positionSpy).size(), 3);
+ QCOMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
+ }
+}
+
+void tst_QMediaPlayerBackend::infiteLoops()
+{
+ if (localVideoFile2.isEmpty())
+ QSKIP("Video format is not supported");
+
+#ifdef Q_OS_MACOS
+ if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci")
+ QSKIP("The test accidently gets crashed on macOS CI, not reproduced locally. To be "
+ "investigated: QTBUG-111744");
+#endif
+
+ TestVideoSink surface(false);
+ QMediaPlayer player;
+
+ player.setVideoOutput(&surface);
+
+ QCOMPARE(player.loops(), 1);
+ player.setLoops(QMediaPlayer::Infinite);
+ QCOMPARE(player.loops(), QMediaPlayer::Infinite);
+
+ // select some small file
+ player.setSource(localVideoFile2);
+ player.setPlaybackRate(20);
+
+ player.play();
+ surface.waitForFrame();
+
+ for (int i = 0; i < 2; ++i) {
+ QSignalSpy positionSpy(&player, &QMediaPlayer::positionChanged);
+
+ QTest::qWait(
+ std::max(static_cast<int>(player.duration() / player.playbackRate() * 4),
+ 300 /*ensure some minimum waiting time to reduce threading flakiness*/));
+ QCOMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia);
+ QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState);
+
+ const auto intervals = positionChangingIntervals(positionSpy);
+ QVERIFY(!intervals.empty());
+ QCOMPARE(intervals.front().second, player.duration());
+ }
+
+ player.stop(); // QMediaPlayer::stop stops whether or not looping is infinite
+ QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState);
+}
+
QTEST_MAIN(tst_QMediaPlayerBackend)
#include "tst_qmediaplayerbackend.moc"