Implementation
EncodedVp8Frame encodeFrame({
required Uint8List srcY,
required Uint8List srcU,
required Uint8List srcV,
required int srcYStride,
required int srcUvStride,
bool? forceKey,
int? qiOverride,
}) {
if (qiOverride != null && (qiOverride < 0 || qiOverride > 127)) {
throw ArgumentError('qiOverride must be in [0,127] (got $qiOverride)');
}
final sw = Stopwatch()..start();
final hardKey = forceKey == true ||
_prevLast == null ||
_framesSinceKey >= keyframeInterval;
var isKey = hardKey || _pendingKeyRequest;
final rc = rateController;
// Phase 22 — escalate to keyframe when adaptive triggers fire.
// Done BEFORE the drop check so a recovery key isn't itself dropped.
if (!isKey) {
if (rc != null &&
forceKeyAfterConsecutiveDrops > 0 &&
rc.consecutiveDrops >= forceKeyAfterConsecutiveDrops) {
isKey = true;
} else if (sceneCutMeanAbsDiff != null && _prevLast != null) {
final m = _meanAbsDiffY(srcY, srcYStride, _prevLast!);
if (m > sceneCutMeanAbsDiff!) {
isKey = true;
}
}
}
// Phase 34 — minKeyframeInterval veto: defer soft KF triggers
// (request / scene-cut / drop-recovery) when the previous KF is
// too recent. Hard triggers (forceKey, missing reference, hitting
// the keyframeInterval cap) always win. A pending request that is
// vetoed here remains pending and fires on the first eligible
// frame.
if (isKey &&
!hardKey &&
minKeyframeInterval > 0 &&
_framesSinceKey < minKeyframeInterval) {
isKey = false;
} else {
_pendingKeyRequest = false;
}
// Rate-controller frame drop: only for inter frames; preserves all
// reference buffers so the next encoded frame still motion-searches
// against what the decoder last received.
if (!isKey && rc != null && rc.shouldDrop(isKey: false)) {
rc.noteDrop();
_framesSinceKey += 1;
_lastStats = null;
_cum._noteDrop();
onFrameDropped?.call();
return EncodedVp8Frame(
bytes: Uint8List(0),
isKey: false,
reconY: Uint8List(0),
reconU: Uint8List(0),
reconV: Uint8List(0),
yStride: 0,
uvStride: 0,
isDropped: true,
);
}
final pickedQi = qiOverride ?? (rc != null ? rc.pickQi(isKey: isKey) : qi);
final frameQi =
pickedQi < minQi ? minQi : (pickedQi > maxQi ? maxQi : pickedQi);
final frameSadPerBit16 =
useMvRateCost ? computeRdConsts(frameQi).sadPerBit16 : 0;
final frameFilterLevel = autoLoopFilter
? deriveLoopFilterLevel(frameQi,
min: autoLoopFilterMin, max: autoLoopFilterMax)
: filterLevel;
if (isKey) {
// A keyframe always re-seeds GOLDEN and ALTREF, so any pending
// refresh requests are satisfied implicitly.
_pendingGoldenRequest = false;
_pendingAltrefRequest = false;
final segPair = _buildSegmentation(width, height, srcY, srcYStride);
final enc = encodeKeyframe(
width: width,
height: height,
qi: frameQi,
srcY: srcY,
srcU: srcU,
srcV: srcV,
srcYStride: srcYStride,
srcUvStride: srcUvStride,
filterLevel: frameFilterLevel,
sharpness: sharpness,
filterType: filterType,
mbNoCoeffSkip: mbNoCoeffSkip,
probSkipFalse: probSkipFalse,
log2NumTokenPartitions: log2NumTokenPartitions,
segmentation: segPair?.spec,
perMbSegmentIds: segPair?.ids,
rdSadPerBit16: useRdIntraMode ? frameSadPerBit16 : 0,
kfYModeProbs: useRdIntraMode ? kfYModeProb : null,
kfUvModeProbs: useRdIntraMode ? kfUvModeProb : null,
);
final fresh = _refFromEnc(enc);
_prevLast = fresh;
_prevGolden = cloneRefFrame(fresh);
_prevAltref = cloneRefFrame(fresh);
_framesSinceKey = 1;
rc?.update(frameBytes: enc.bytes.length, isKey: true);
final mbCountKf = (width >> 4) * (height >> 4);
final statsKf = Vp8FrameStats(
frameIndex: _cum._framesEncoded,
isKeyframe: true,
qi: frameQi,
filterLevel: frameFilterLevel,
totalMbCount: mbCountKf,
intraMbCount: mbCountKf,
zeroMvMbCount: 0,
nearestMvMbCount: 0,
nearMvMbCount: 0,
newMvMbCount: 0,
newMvLastMbCount: 0,
newMvGoldenMbCount: 0,
newMvAltrefMbCount: 0,
goldenRefMbCount: 0,
altrefRefMbCount: 0,
denoisedMbCount: 0,
staticShortCircuitMbCount: 0,
bytesEmitted: enc.bytes.length,
meanAbsMvQuarterPel: 0.0,
maxAbsMvQuarterPel: 0,
encodeDurationMicros: sw.elapsedMicroseconds,
psnrY: _psnrY(srcY, srcYStride, enc.reconY, enc.yStride, width, height),
);
_lastStats = statsKf;
_cum._accumulate(statsKf, isKey: true);
onFrameEncoded?.call(statsKf);
return EncodedVp8Frame(
bytes: enc.bytes,
isKey: true,
reconY: enc.reconY,
reconU: enc.reconU,
reconV: enc.reconV,
yStride: enc.yStride,
uvStride: enc.uvStride,
stats: statsKf,
);
}
final prevLast = _prevLast!;
final prevGolden = _prevGolden!;
final prevAltref = _prevAltref!;
final mbCols = width >> 4;
final mbRows = height >> 4;
// Optional temporal denoise on Y against the previous reconstruction.
// The result replaces `srcY` for both motion search and the encode.
Uint8List effSrcY = srcY;
int effSrcYStride = srcYStride;
final dn = denoiser;
if (dn != null) {
effSrcY = dn.denoiseY(
width: width,
height: height,
srcY: srcY,
srcYStride: srcYStride,
prevRef: prevLast,
);
effSrcYStride = srcYStride;
}
final clamped = searchRange.clamp(0, 16);
final signBias = <bool>[false, false, false, false];
final miList = List<ModeInfo>.generate(mbCols * mbRows, (_) => ModeInfo());
final nearMvs = List<Mv>.generate(4, (_) => Mv(0, 0));
final cnt = Int32List(4);
final predBuf = Uint8List(256);
final specs = <InterMbSpec>[];
// Build the set of MB indices forced to intra-refresh this frame.
final mbCount = mbCols * mbRows;
final cirCount = cyclicRefreshPercent == 0
? 0
: ((cyclicRefreshPercent * mbCount + 50) ~/ 100).clamp(0, mbCount);
final cirSet = cirCount == 0 ? null : <int>{};
if (cirSet != null) {
for (var k = 0; k < cirCount; k++) {
cirSet.add((_cirCursor + k) % mbCount);
}
_cirCursor = (_cirCursor + cirCount) % mbCount;
}
var staticShortCircuitMbCount = 0;
for (var r = 0; r < mbRows; r++) {
for (var c = 0; c < mbCols; c++) {
final mbIdx = r * mbCols + c;
final mi = miList[mbIdx];
// Cyclic intra refresh: force this MB to be coded as intra.
if (cirSet != null && cirSet.contains(mbIdx)) {
mi.refFrame = refIntra;
mi.yMode = dcPred;
mi.uvMode = dcPred;
mi.mv.row = 0;
mi.mv.col = 0;
specs.add(const InterIntraMbSpec(yMode: dcPred, uvMode: dcPred));
continue;
}
final zeroSad = sad16x16At(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevLast,
mbRow: r,
mbCol: c,
);
// Static-MB short-circuit: when the LAST-ZEROMV SAD is at or
// below the configured threshold, lock the MB to LAST-ZEROMV
// and skip every other candidate (NEAREST/NEAR/NEW/GOLDEN).
if (staticMbSadThreshold > 0 && zeroSad <= staticMbSadThreshold) {
mi.refFrame = refLast;
mi.yMode = zeroMv;
mi.mv.row = 0;
mi.mv.col = 0;
specs.add(const InterZeroMvMbSpec(refFrame: refLast));
staticShortCircuitMbCount += 1;
continue;
}
InterMbSpec pick;
var bestSad = zeroSad;
Mv bestMv = Mv(0, 0);
int bestMode = 0; // 0=ZERO, 1=NEAREST, 2=NEAR, 3=NEW
// P68: mv-ref probability context for the optional Lagrangian
// per-mode bias. Defaulted to vp8_mode_contexts row 0
// (cnt = [0,0,0,0]); recomputed below if findNearMvs runs.
var mvRefProbs = const <int>[7, 1, 1, 143];
int rdBias(int modeToken, [Mv? mv]) {
if (!useRdInterMode || frameSadPerBit16 <= 0) return 0;
var rate = mvRefModeBitCost(modeToken, mvRefProbs);
if (modeToken == newMv) rate += mvBitCost(mv!, defaultMvContext);
return (frameSadPerBit16 * rate) >> 8;
}
// Predictor candidates (NEAREST/NEAR), if enabled.
Mv nearestCand = Mv(0, 0);
Mv nearCand = Mv(0, 0);
var haveNearest = false;
var haveNear = false;
if (predictorMotionSearch) {
final above = r > 0 ? miList[(r - 1) * mbCols + c] : offFrameMi;
final left = c > 0 ? miList[r * mbCols + (c - 1)] : offFrameMi;
final aboveLeft = (r > 0 && c > 0)
? miList[(r - 1) * mbCols + (c - 1)]
: offFrameMi;
findNearMvs(
above: above,
left: left,
aboveLeft: aboveLeft,
refFrame: refLast,
signBias: signBias,
nearMvs: nearMvs,
cnt: cnt,
);
// Swap+tiebreak (mirrors writer's NEAREST/NEAR path).
if (cnt[3] > 0 && nearMvs[3].asInt == nearMvs[1].asInt) {
cnt[1] += 1;
}
if (cnt[2] > cnt[1]) {
final tc = cnt[1];
cnt[1] = cnt[2];
cnt[2] = tc;
final tr = nearMvs[1].row;
final tco = nearMvs[1].col;
nearMvs[1].row = nearMvs[2].row;
nearMvs[1].col = nearMvs[2].col;
nearMvs[2].row = tr;
nearMvs[2].col = tco;
}
if (useRdInterMode) {
mvRefProbs = mvRefProbsFromCnt(cnt);
}
// ZEROMV baseline bias is added now that mvRefProbs is final.
bestSad += rdBias(zeroMv);
final mbToLeftEdge = -((c * 16) << 3) - mvMarginEdge;
final mbToRightEdge = (((mbCols - 1 - c) * 16) << 3) + mvMarginEdge;
final mbToTopEdge = -((r * 16) << 3) - mvMarginEdge;
final mbToBottomEdge = (((mbRows - 1 - r) * 16) << 3) + mvMarginEdge;
// NEAREST candidate = nearMvs[1] after clamp.
final nm1 = Mv(nearMvs[1].row, nearMvs[1].col);
clampMv2(
nm1, mbToLeftEdge, mbToRightEdge, mbToTopEdge, mbToBottomEdge);
if (nm1.row != 0 || nm1.col != 0) {
nearestCand = nm1;
haveNearest = true;
final sad = sad16x16Subpel(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevLast,
mbRow: r,
mbCol: c,
mv: nm1,
predBuf: predBuf,
);
if (sad + (useRdInterMode ? rdBias(nearestMv) : nearestSadBias) <
bestSad) {
bestSad =
sad + (useRdInterMode ? rdBias(nearestMv) : nearestSadBias);
bestMv = nm1;
bestMode = 1;
}
}
// NEAR candidate = nearMvs[2] after clamp.
final nm2 = Mv(nearMvs[2].row, nearMvs[2].col);
clampMv2(
nm2, mbToLeftEdge, mbToRightEdge, mbToTopEdge, mbToBottomEdge);
if (nm2.row != 0 || nm2.col != 0) {
nearCand = nm2;
haveNear = true;
final sad = sad16x16Subpel(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevLast,
mbRow: r,
mbCol: c,
mv: nm2,
predBuf: predBuf,
);
if (sad + (useRdInterMode ? rdBias(nearMv) : nearSadBias) <
bestSad) {
bestSad = sad + (useRdInterMode ? rdBias(nearMv) : nearSadBias);
bestMv = nm2;
bestMode = 2;
}
}
}
// NEW candidate: integer search (seeded at nearest predictor if
// we have one) + sub-pel refinement.
if (clamped > 0) {
// If predictorMotionSearch ran above, bestSad already includes
// the ZEROMV RD bias; if not, add it here so NEW competes on
// the same footing.
if (!predictorMotionSearch) bestSad += rdBias(zeroMv);
final MotionSearchResult integer;
if (useDiamondMotionSearch) {
integer = diamondIntegerMotionSearch(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevLast,
mbRow: r,
mbCol: c,
searchRange: clamped,
mvRateBias: mvRateBias,
sadPerBit: frameSadPerBit16,
seedMv: predictorMotionSearch && haveNearest ? nearestCand : null,
);
} else if (predictorMotionSearch && haveNearest) {
integer = naiveIntegerMotionSearchSeeded(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevLast,
mbRow: r,
mbCol: c,
centerMv: nearestCand,
searchRange: clamped,
mvRateBias: mvRateBias,
sadPerBit: frameSadPerBit16,
);
} else {
integer = naiveIntegerMotionSearch(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevLast,
mbRow: r,
mbCol: c,
searchRange: clamped,
mvRateBias: mvRateBias,
sadPerBit: frameSadPerBit16,
);
}
final refined = subPelRefine
? subPelRefineSearch(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevLast,
mbRow: r,
mbCol: c,
integerMv: integer.mv,
sadPerBit: frameSadPerBit16,
)
: integer;
if (refined.mv.row != 0 || refined.mv.col != 0) {
final newBias = useRdInterMode
? rdBias(newMv, refined.mv)
: (zeroMvSadBias > newMvSadBias ? zeroMvSadBias : newMvSadBias);
if (refined.sad + newBias < bestSad) {
bestSad = refined.sad + newBias;
bestMv = refined.mv;
bestMode = 3;
}
}
}
switch (bestMode) {
case 1:
pick = InterNearestMvMbSpec(refFrame: refLast);
mi.refFrame = refLast;
mi.yMode = nearestMv;
mi.mv.row = nearestCand.row;
mi.mv.col = nearestCand.col;
break;
case 2:
pick = InterNearMvMbSpec(refFrame: refLast);
mi.refFrame = refLast;
mi.yMode = nearMv;
mi.mv.row = nearCand.row;
mi.mv.col = nearCand.col;
break;
case 3:
pick = InterNewMvMbSpec(mv: bestMv, refFrame: refLast);
mi.refFrame = refLast;
mi.yMode = newMv;
mi.mv.row = bestMv.row;
mi.mv.col = bestMv.col;
break;
default:
pick = const InterZeroMvMbSpec(refFrame: refLast);
mi.refFrame = refLast;
mi.yMode = zeroMv;
mi.mv.row = 0;
mi.mv.col = 0;
}
// P69: cross-ref RD bias helper. When useRdInterMode is on we
// re-base bestSad so it represents the LAST pick's *full*
// signalling rate (mode tree + refFrameBitCost(refLast)) and
// each cross-ref candidate is compared with its own full rate
// (refFrameBitCost(refX) + mvRefModeBitCost + maybe mvBitCost).
// Cross-ref mvRefProbs default to vp8_mode_contexts row 0
// because the per-ref cnt vector is not recomputed.
const crossProbLast = 128, crossProbGf = 128;
const crossMvRefProbs = <int>[7, 1, 1, 143];
int crossRefRdBias(int refFrame, int modeToken, [Mv? mv]) {
if (!useRdInterMode || frameSadPerBit16 <= 0) return 0;
var rate = refFrameBitCost(refFrame, crossProbLast, crossProbGf) +
mvRefModeBitCost(modeToken, crossMvRefProbs);
if (modeToken == newMv) rate += mvBitCost(mv!, defaultMvContext);
return (frameSadPerBit16 * rate) >> 8;
}
if (useRdInterMode && frameSadPerBit16 > 0) {
bestSad += (frameSadPerBit16 *
refFrameBitCost(refLast, crossProbLast, crossProbGf)) >>
8;
}
// Optional: NEWMV search against the GOLDEN reference. If a
// valid (non-zero) refined MV beats the current best by
// [goldenNewMvSadBias], switch the MB to NEWMV-from-GOLDEN.
if (tryGoldenNewMv && clamped > 0) {
final integerG = useDiamondMotionSearch
? diamondIntegerMotionSearch(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevGolden,
mbRow: r,
mbCol: c,
searchRange: clamped,
mvRateBias: mvRateBias,
sadPerBit: frameSadPerBit16,
)
: naiveIntegerMotionSearch(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevGolden,
mbRow: r,
mbCol: c,
searchRange: clamped,
mvRateBias: mvRateBias,
sadPerBit: frameSadPerBit16,
);
final refinedG = subPelRefine
? subPelRefineSearch(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevGolden,
mbRow: r,
mbCol: c,
integerMv: integerG.mv,
sadPerBit: frameSadPerBit16,
)
: integerG;
if (refinedG.mv.row != 0 || refinedG.mv.col != 0) {
final gBias = useRdInterMode
? crossRefRdBias(refGolden, newMv, refinedG.mv)
: goldenNewMvSadBias;
if (refinedG.sad + gBias < bestSad) {
bestSad = refinedG.sad + gBias;
pick = InterNewMvMbSpec(mv: refinedG.mv, refFrame: refGolden);
mi.refFrame = refGolden;
mi.yMode = newMv;
mi.mv.row = refinedG.mv.row;
mi.mv.col = refinedG.mv.col;
}
}
}
// Optional: also try ZEROMV against the GOLDEN reference. If it
// beats the LAST-relative best by [goldenSadBias], switch the
// MB's ref to GOLDEN (still ZEROMV, no MV bits spent).
if (tryGoldenZeroMv) {
final goldenZeroSad = sad16x16At(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevGolden,
mbRow: r,
mbCol: c,
);
if (goldenZeroSad +
(useRdInterMode
? crossRefRdBias(refGolden, zeroMv)
: goldenSadBias) <
bestSad) {
pick = const InterZeroMvMbSpec(refFrame: refGolden);
mi.refFrame = refGolden;
mi.yMode = zeroMv;
mi.mv.row = 0;
mi.mv.col = 0;
bestSad = goldenZeroSad +
(useRdInterMode ? crossRefRdBias(refGolden, zeroMv) : 0);
}
}
// Symmetric ALTREF ZEROMV eval. Compared against the current
// best (which may already be GOLDEN); the winner of the three
// refs takes the slot.
if (tryAltrefZeroMv) {
final altrefZeroSad = sad16x16At(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevAltref,
mbRow: r,
mbCol: c,
);
if (altrefZeroSad +
(useRdInterMode
? crossRefRdBias(refAltref, zeroMv)
: altrefSadBias) <
bestSad) {
pick = const InterZeroMvMbSpec(refFrame: refAltref);
mi.refFrame = refAltref;
mi.yMode = zeroMv;
mi.mv.row = 0;
mi.mv.col = 0;
bestSad = altrefZeroSad +
(useRdInterMode ? crossRefRdBias(refAltref, zeroMv) : 0);
}
}
// Symmetric ALTREF NEWMV search. Mirrors the GOLDEN NEWMV
// block above.
if (tryAltrefNewMv && clamped > 0) {
final integerA = useDiamondMotionSearch
? diamondIntegerMotionSearch(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevAltref,
mbRow: r,
mbCol: c,
searchRange: clamped,
mvRateBias: mvRateBias,
sadPerBit: frameSadPerBit16,
)
: naiveIntegerMotionSearch(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevAltref,
mbRow: r,
mbCol: c,
searchRange: clamped,
mvRateBias: mvRateBias,
sadPerBit: frameSadPerBit16,
);
final refinedA = subPelRefine
? subPelRefineSearch(
srcY: effSrcY,
srcYStride: effSrcYStride,
ref: prevAltref,
mbRow: r,
mbCol: c,
integerMv: integerA.mv,
sadPerBit: frameSadPerBit16,
)
: integerA;
if (refinedA.mv.row != 0 || refinedA.mv.col != 0) {
final aBias = useRdInterMode
? crossRefRdBias(refAltref, newMv, refinedA.mv)
: altrefNewMvSadBias;
if (refinedA.sad + aBias < bestSad) {
bestSad = refinedA.sad + aBias;
pick = InterNewMvMbSpec(mv: refinedA.mv, refFrame: refAltref);
mi.refFrame = refAltref;
mi.yMode = newMv;
mi.mv.row = refinedA.mv.row;
mi.mv.col = refinedA.mv.col;
}
}
}
specs.add(pick);
}
}
final segPair = _buildSegmentation(width, height, effSrcY, effSrcYStride);
// Decide whether to refresh GOLDEN this inter. With a non-zero
// [goldenRefreshPeriod] we refresh exactly every Nth inter since
// the last keyframe, ignoring [refreshGoldenOnInter]; otherwise we
// honour the static flag.
final interIndex = _framesSinceKey + 1;
final periodicGoldenRefresh = goldenRefreshPeriod > 0
? (interIndex % goldenRefreshPeriod == 0)
: refreshGoldenOnInter;
final refreshGoldenThisFrame =
_pendingGoldenRequest || periodicGoldenRefresh;
// Symmetric ALTREF refresh decision.
final periodicAltrefRefresh = altrefRefreshPeriod > 0
? (interIndex % altrefRefreshPeriod == 0)
: refreshAltrefOnInter;
final refreshAltrefThisFrame =
_pendingAltrefRequest || periodicAltrefRefresh;
// Consume the external request — the next inter is now guaranteed
// to carry a fresh golden, so subsequent inters revert to the
// configured periodic/static schedule.
_pendingGoldenRequest = false;
_pendingAltrefRequest = false;
final enc = encodeInterFrame(
width: width,
height: height,
qi: frameQi,
srcY: effSrcY,
srcU: srcU,
srcV: srcV,
srcYStride: effSrcYStride,
srcUvStride: srcUvStride,
mbs: specs,
prevRef: prevLast,
prevGolden: prevGolden,
prevAltref: prevAltref,
filterLevel: frameFilterLevel,
sharpness: sharpness,
filterType: filterType,
mbNoCoeffSkip: mbNoCoeffSkip,
probSkipFalse: probSkipFalse,
refreshLast: true,
refreshGolden: refreshGoldenThisFrame,
refreshAltref: refreshAltrefThisFrame,
log2NumTokenPartitions: log2NumTokenPartitions,
segmentation: segPair?.spec,
perMbSegmentIds: segPair?.ids,
);
final fresh = _refFromEnc(enc);
_prevLast = fresh;
if (refreshGoldenThisFrame) {
_prevGolden = cloneRefFrame(fresh);
}
if (refreshAltrefThisFrame) {
_prevAltref = cloneRefFrame(fresh);
}
_framesSinceKey += 1;
rc?.update(frameBytes: enc.bytes.length, isKey: false);
// Stats: walk specs/miList once to tally MB-mode + ref counts.
var intraMbCount = 0;
var zeroMvMbCount = 0;
var nearestMvMbCount = 0;
var nearMvMbCount = 0;
var newMvMbCount = 0;
var newMvLastMbCount = 0;
var newMvGoldenMbCount = 0;
var newMvAltrefMbCount = 0;
var goldenRefMbCount = 0;
var altrefRefMbCount = 0;
var mvAbsSum = 0;
var mvAbsMax = 0;
var mvCount = 0;
for (var i = 0; i < specs.length; i++) {
final m = miList[i];
if (m.refFrame == refIntra) {
intraMbCount += 1;
continue;
}
if (m.refFrame == refGolden) goldenRefMbCount += 1;
if (m.refFrame == refAltref) altrefRefMbCount += 1;
final ar = m.mv.row >= 0 ? m.mv.row : -m.mv.row;
final ac = m.mv.col >= 0 ? m.mv.col : -m.mv.col;
final amax = ar > ac ? ar : ac;
mvAbsSum += amax;
if (amax > mvAbsMax) mvAbsMax = amax;
mvCount += 1;
switch (m.yMode) {
case nearestMv:
nearestMvMbCount += 1;
break;
case nearMv:
nearMvMbCount += 1;
break;
case newMv:
newMvMbCount += 1;
if (m.refFrame == refGolden) {
newMvGoldenMbCount += 1;
} else if (m.refFrame == refAltref) {
newMvAltrefMbCount += 1;
} else {
newMvLastMbCount += 1;
}
break;
default:
zeroMvMbCount += 1;
}
}
final stats = Vp8FrameStats(
frameIndex: _cum._framesEncoded,
isKeyframe: false,
qi: frameQi,
filterLevel: frameFilterLevel,
totalMbCount: mbCount,
intraMbCount: intraMbCount,
zeroMvMbCount: zeroMvMbCount,
nearestMvMbCount: nearestMvMbCount,
nearMvMbCount: nearMvMbCount,
newMvMbCount: newMvMbCount,
newMvLastMbCount: newMvLastMbCount,
newMvGoldenMbCount: newMvGoldenMbCount,
newMvAltrefMbCount: newMvAltrefMbCount,
goldenRefMbCount: goldenRefMbCount,
altrefRefMbCount: altrefRefMbCount,
denoisedMbCount: dn?.lastDenoisedMbCount ?? 0,
staticShortCircuitMbCount: staticShortCircuitMbCount,
bytesEmitted: enc.bytes.length,
meanAbsMvQuarterPel: mvCount == 0 ? 0.0 : mvAbsSum / mvCount,
maxAbsMvQuarterPel: mvAbsMax,
encodeDurationMicros: sw.elapsedMicroseconds,
psnrY: _psnrY(srcY, srcYStride, enc.reconY, enc.yStride, width, height),
);
_lastStats = stats;
_cum._accumulate(stats, isKey: false);
onFrameEncoded?.call(stats);
return EncodedVp8Frame(
bytes: enc.bytes,
isKey: false,
reconY: enc.reconY,
reconU: enc.reconU,
reconV: enc.reconV,
yStride: enc.yStride,
uvStride: enc.uvStride,
stats: stats,
);
}