mirror of
https://github.com/androidx/media.git
synced 2025-05-17 12:39:52 +08:00
Create frame processor in MCVR
PiperOrigin-RevId: 495368262
This commit is contained in:
parent
91557ac9d4
commit
4398af3a4d
@ -1097,6 +1097,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
if (codecOperatingRate <= assumedMinimumCodecOperatingRate) {
|
if (codecOperatingRate <= assumedMinimumCodecOperatingRate) {
|
||||||
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
|
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
|
||||||
}
|
}
|
||||||
|
onReadyToInitializeCodec(inputFormat);
|
||||||
codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||||
MediaCodecAdapter.Configuration configuration =
|
MediaCodecAdapter.Configuration configuration =
|
||||||
getMediaCodecConfiguration(codecInfo, inputFormat, crypto, codecOperatingRate);
|
getMediaCodecConfiguration(codecInfo, inputFormat, crypto, codecOperatingRate);
|
||||||
@ -1381,6 +1382,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when ready to initialize the {@link MediaCodecAdapter}.
|
||||||
|
*
|
||||||
|
* <p>This method is called just before the renderer obtains the {@linkplain
|
||||||
|
* #getMediaCodecConfiguration configuration} for the {@link MediaCodecAdapter} and creates the
|
||||||
|
* adapter via the passed in {@link MediaCodecAdapter.Factory}.
|
||||||
|
*
|
||||||
|
* <p>The default implementation is a no-op.
|
||||||
|
*
|
||||||
|
* @param format The {@link Format} for which the codec is being configured.
|
||||||
|
* @throws ExoPlaybackException If an error occurs preparing for initializing the codec.
|
||||||
|
*/
|
||||||
|
protected void onReadyToInitializeCodec(Format format) throws ExoPlaybackException {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a {@link MediaCodec} has been created and configured.
|
* Called when a {@link MediaCodec} has been created and configured.
|
||||||
*
|
*
|
||||||
|
@ -50,6 +50,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
|
|||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.FormatHolder;
|
import com.google.android.exoplayer2.FormatHolder;
|
||||||
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||||
import com.google.android.exoplayer2.RendererCapabilities;
|
import com.google.android.exoplayer2.RendererCapabilities;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||||
@ -64,15 +65,27 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
|
|||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||||
|
import com.google.android.exoplayer2.util.DebugViewProvider;
|
||||||
|
import com.google.android.exoplayer2.util.Effect;
|
||||||
|
import com.google.android.exoplayer2.util.FrameInfo;
|
||||||
|
import com.google.android.exoplayer2.util.FrameProcessingException;
|
||||||
|
import com.google.android.exoplayer2.util.FrameProcessor;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.MediaFormatUtil;
|
import com.google.android.exoplayer2.util.MediaFormatUtil;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer2.util.Size;
|
||||||
|
import com.google.android.exoplayer2.util.SurfaceInfo;
|
||||||
import com.google.android.exoplayer2.util.TraceUtil;
|
import com.google.android.exoplayer2.util.TraceUtil;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
|
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import org.checkerframework.checker.initialization.qual.UnderInitialization;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes and renders video using {@link MediaCodec}.
|
* Decodes and renders video using {@link MediaCodec}.
|
||||||
@ -84,6 +97,8 @@ import java.util.List;
|
|||||||
* <li>Message with type {@link #MSG_SET_VIDEO_OUTPUT} to set the output. The message payload
|
* <li>Message with type {@link #MSG_SET_VIDEO_OUTPUT} to set the output. The message payload
|
||||||
* should be the target {@link Surface}, or null to clear the output. Other non-null payloads
|
* should be the target {@link Surface}, or null to clear the output. Other non-null payloads
|
||||||
* have the effect of clearing the output.
|
* have the effect of clearing the output.
|
||||||
|
* <li>Message with type {@link #MSG_SET_VIDEO_OUTPUT_RESOLUTION} to set the output resolution.
|
||||||
|
* The message payload should be the output resolution in {@link Size}.
|
||||||
* <li>Message with type {@link #MSG_SET_SCALING_MODE} to set the video scaling mode. The message
|
* <li>Message with type {@link #MSG_SET_SCALING_MODE} to set the video scaling mode. The message
|
||||||
* payload should be one of the integer scaling modes in {@link C.VideoScalingMode}. Note that
|
* payload should be one of the integer scaling modes in {@link C.VideoScalingMode}. Note that
|
||||||
* the scaling mode only applies if the {@link Surface} targeted by this renderer is owned by
|
* the scaling mode only applies if the {@link Surface} targeted by this renderer is owned by
|
||||||
@ -125,6 +140,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
private final Context context;
|
private final Context context;
|
||||||
private final VideoFrameReleaseHelper frameReleaseHelper;
|
private final VideoFrameReleaseHelper frameReleaseHelper;
|
||||||
private final EventDispatcher eventDispatcher;
|
private final EventDispatcher eventDispatcher;
|
||||||
|
private final FrameProcessorManager frameProcessorManager;
|
||||||
private final long allowedJoiningTimeMs;
|
private final long allowedJoiningTimeMs;
|
||||||
private final int maxDroppedFramesToNotify;
|
private final int maxDroppedFramesToNotify;
|
||||||
private final boolean deviceNeedsNoPostProcessWorkaround;
|
private final boolean deviceNeedsNoPostProcessWorkaround;
|
||||||
@ -328,6 +344,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
frameReleaseHelper = new VideoFrameReleaseHelper(this.context);
|
frameReleaseHelper = new VideoFrameReleaseHelper(this.context);
|
||||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||||
|
frameProcessorManager = new FrameProcessorManager(this);
|
||||||
deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround();
|
deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround();
|
||||||
joiningDeadlineMs = C.TIME_UNSET;
|
joiningDeadlineMs = C.TIME_UNSET;
|
||||||
scalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
|
scalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
|
||||||
@ -612,6 +629,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
try {
|
try {
|
||||||
super.onReset();
|
super.onReset();
|
||||||
} finally {
|
} finally {
|
||||||
|
if (frameProcessorManager.isEnabled()) {
|
||||||
|
frameProcessorManager.reset();
|
||||||
|
}
|
||||||
if (placeholderSurface != null) {
|
if (placeholderSurface != null) {
|
||||||
releasePlaceholderSurface();
|
releasePlaceholderSurface();
|
||||||
}
|
}
|
||||||
@ -647,6 +667,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case MSG_SET_VIDEO_OUTPUT_RESOLUTION:
|
||||||
|
Size outputResolution = (Size) checkNotNull(message);
|
||||||
|
if (displaySurface != null && frameProcessorManager.isEnabled()) {
|
||||||
|
frameProcessorManager.setOutputSurfaceInfo(displaySurface, outputResolution);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case MSG_SET_AUDIO_ATTRIBUTES:
|
case MSG_SET_AUDIO_ATTRIBUTES:
|
||||||
case MSG_SET_AUX_EFFECT_INFO:
|
case MSG_SET_AUX_EFFECT_INFO:
|
||||||
case MSG_SET_CAMERA_MOTION_LISTENER:
|
case MSG_SET_CAMERA_MOTION_LISTENER:
|
||||||
@ -659,6 +685,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setOutput(@Nullable Object output) throws ExoPlaybackException {
|
private void setOutput(@Nullable Object output) throws ExoPlaybackException {
|
||||||
|
// TODO(b/238302341) Handle output surface change in previewing.
|
||||||
// Handle unsupported (i.e., non-Surface) outputs by clearing the display surface.
|
// Handle unsupported (i.e., non-Surface) outputs by clearing the display surface.
|
||||||
@Nullable Surface displaySurface = output instanceof Surface ? (Surface) output : null;
|
@Nullable Surface displaySurface = output instanceof Surface ? (Surface) output : null;
|
||||||
|
|
||||||
@ -753,8 +780,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
}
|
}
|
||||||
displaySurface = placeholderSurface;
|
displaySurface = placeholderSurface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frameProcessorManager.isEnabled()) {
|
||||||
|
mediaFormat = frameProcessorManager.amendMediaFormatKeys(mediaFormat);
|
||||||
|
}
|
||||||
|
|
||||||
return MediaCodecAdapter.Configuration.createForVideoDecoding(
|
return MediaCodecAdapter.Configuration.createForVideoDecoding(
|
||||||
codecInfo, mediaFormat, format, displaySurface, crypto);
|
codecInfo,
|
||||||
|
mediaFormat,
|
||||||
|
format,
|
||||||
|
frameProcessorManager.isEnabled()
|
||||||
|
? frameProcessorManager.getInputSurface()
|
||||||
|
: displaySurface,
|
||||||
|
crypto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -878,6 +916,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
return maxFrameRate == -1 ? CODEC_OPERATING_RATE_UNSET : (maxFrameRate * targetPlaybackSpeed);
|
return maxFrameRate == -1 ? CODEC_OPERATING_RATE_UNSET : (maxFrameRate * targetPlaybackSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
@Override
|
||||||
|
protected void onReadyToInitializeCodec(Format format) throws ExoPlaybackException {
|
||||||
|
if (!frameProcessorManager.isEnabled()) {
|
||||||
|
frameProcessorManager.maybeEnable(format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCodecInitialized(
|
protected void onCodecInitialized(
|
||||||
String name,
|
String name,
|
||||||
@ -985,6 +1031,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
decodedVideoSize =
|
decodedVideoSize =
|
||||||
new VideoSize(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
|
new VideoSize(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
|
||||||
frameReleaseHelper.onFormatChanged(format.frameRate);
|
frameReleaseHelper.onFormatChanged(format.frameRate);
|
||||||
|
|
||||||
|
if (frameProcessorManager.isEnabled()) {
|
||||||
|
frameProcessorManager.setInputFrameInfo(width, height, pixelWidthHeightRatio);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1622,6 +1672,185 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
return new MediaCodecVideoDecoderException(cause, codecInfo, displaySurface);
|
return new MediaCodecVideoDecoderException(cause, codecInfo, displaySurface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Manages {@link FrameProcessor} interactions. */
|
||||||
|
private static final class FrameProcessorManager {
|
||||||
|
|
||||||
|
private static final String FRAME_PROCESSOR_FACTORY_CLASS =
|
||||||
|
"com.google.android.exoplayer2.effect.GlEffectsFrameProcessor$Factory";
|
||||||
|
|
||||||
|
// TODO(b/238302341) Consider removing the reference to the containing class and make this class
|
||||||
|
// non-static.
|
||||||
|
private final MediaCodecVideoRenderer renderer;
|
||||||
|
private final ArrayDeque<Long> processedFrames;
|
||||||
|
|
||||||
|
private @MonotonicNonNull Handler handler;
|
||||||
|
@Nullable private FrameProcessor frameProcessor;
|
||||||
|
@Nullable private CopyOnWriteArrayList<Effect> videoEffects;
|
||||||
|
private boolean canEnableFrameProcessing;
|
||||||
|
|
||||||
|
/** Creates a new instance. */
|
||||||
|
public FrameProcessorManager(@UnderInitialization MediaCodecVideoRenderer renderer) {
|
||||||
|
this.renderer = renderer;
|
||||||
|
processedFrames = new ArrayDeque<>();
|
||||||
|
canEnableFrameProcessing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the {@linkplain Effect video effects}. */
|
||||||
|
public void setVideoEffects(List<Effect> videoEffects) {
|
||||||
|
if (this.videoEffects == null) {
|
||||||
|
this.videoEffects = new CopyOnWriteArrayList<>(videoEffects);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.videoEffects.clear();
|
||||||
|
this.videoEffects.addAll(videoEffects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether frame processing is enabled. */
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return frameProcessor != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to enable frame processing.
|
||||||
|
*
|
||||||
|
* <p>Caller must ensure frame processing {@linkplain #isEnabled() is not enabled} before
|
||||||
|
* calling this method.
|
||||||
|
*
|
||||||
|
* @param inputFormat The {@link Format} that is input into the {@link FrameProcessor}.
|
||||||
|
* @return Whether frame processing is enabled.
|
||||||
|
* @throws ExoPlaybackException When enabling the {@link FrameProcessor} failed.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public boolean maybeEnable(Format inputFormat) throws ExoPlaybackException {
|
||||||
|
checkState(!isEnabled());
|
||||||
|
if (!canEnableFrameProcessing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (videoEffects == null) {
|
||||||
|
canEnableFrameProcessing = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playback thread handler.
|
||||||
|
handler = Util.createHandlerForCurrentLooper();
|
||||||
|
try {
|
||||||
|
frameProcessor =
|
||||||
|
((FrameProcessor.Factory)
|
||||||
|
Class.forName(FRAME_PROCESSOR_FACTORY_CLASS).getConstructor().newInstance())
|
||||||
|
.create(
|
||||||
|
renderer.context,
|
||||||
|
checkNotNull(videoEffects),
|
||||||
|
DebugViewProvider.NONE,
|
||||||
|
inputFormat.colorInfo != null
|
||||||
|
? inputFormat.colorInfo
|
||||||
|
: ColorInfo.SDR_BT709_LIMITED,
|
||||||
|
/* outputColorInfo= */ ColorInfo.SDR_BT709_LIMITED,
|
||||||
|
/* releaseFramesAutomatically= */ false,
|
||||||
|
/* executor= */ handler::post,
|
||||||
|
new FrameProcessor.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onOutputSizeChanged(int width, int height) {
|
||||||
|
// TODO(b/238302341) Handle output size change.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOutputFrameAvailable(long presentationTimeUs) {
|
||||||
|
processedFrames.add(presentationTimeUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFrameProcessingError(FrameProcessingException exception) {
|
||||||
|
renderer.setPendingPlaybackException(
|
||||||
|
renderer.createRendererException(
|
||||||
|
exception,
|
||||||
|
inputFormat,
|
||||||
|
// TODO(b/238302341) Add relevant error codes for frame processing.
|
||||||
|
PlaybackException.ERROR_CODE_UNSPECIFIED));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFrameProcessingEnded() {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw renderer.createRendererException(
|
||||||
|
e, inputFormat, PlaybackException.ERROR_CODE_UNSPECIFIED);
|
||||||
|
}
|
||||||
|
setInputFrameInfo(inputFormat.width, inputFormat.height, inputFormat.pixelWidthHeightRatio);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@linkplain FrameProcessor#getInputSurface input surface} of the {@link
|
||||||
|
* FrameProcessor}.
|
||||||
|
*
|
||||||
|
* <p>Caller must ensure the {@code FrameProcessorManager} {@link #isEnabled()} before calling
|
||||||
|
* this method.
|
||||||
|
*/
|
||||||
|
public Surface getInputSurface() {
|
||||||
|
return checkNotNull(frameProcessor).getInputSurface();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the output surface info.
|
||||||
|
*
|
||||||
|
* <p>Caller must ensure the {@code FrameProcessorManager} {@link #isEnabled()} before calling
|
||||||
|
* this method.
|
||||||
|
*
|
||||||
|
* @param outputSurface The {@link Surface} to which {@link FrameProcessor} outputs.
|
||||||
|
* @param outputResolution The {@link Size} of the output resolution.
|
||||||
|
*/
|
||||||
|
public void setOutputSurfaceInfo(Surface outputSurface, Size outputResolution) {
|
||||||
|
checkNotNull(frameProcessor)
|
||||||
|
.setOutputSurfaceInfo(
|
||||||
|
new SurfaceInfo(
|
||||||
|
outputSurface, outputResolution.getWidth(), outputResolution.getHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the input surface info.
|
||||||
|
*
|
||||||
|
* <p>Caller must ensure the {@code FrameProcessorManager} {@link #isEnabled()} before calling
|
||||||
|
* this method.
|
||||||
|
*/
|
||||||
|
public void setInputFrameInfo(int width, int height, float pixelWidthHeightRatio) {
|
||||||
|
checkNotNull(frameProcessor)
|
||||||
|
.setInputFrameInfo(
|
||||||
|
new FrameInfo(
|
||||||
|
width, height, pixelWidthHeightRatio, renderer.getOutputStreamOffsetUs()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the necessary {@link MediaFormat} keys for frame processing. */
|
||||||
|
@SuppressWarnings("InlinedApi")
|
||||||
|
public MediaFormat amendMediaFormatKeys(MediaFormat mediaFormat) {
|
||||||
|
if (Util.SDK_INT >= 29
|
||||||
|
&& renderer.context.getApplicationContext().getApplicationInfo().targetSdkVersion >= 29) {
|
||||||
|
mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0);
|
||||||
|
}
|
||||||
|
return mediaFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases the resources.
|
||||||
|
*
|
||||||
|
* <p>Caller must ensure frame processing {@linkplain #isEnabled() is not enabled} before
|
||||||
|
* calling this method.
|
||||||
|
*/
|
||||||
|
public void reset() {
|
||||||
|
checkNotNull(frameProcessor).release();
|
||||||
|
frameProcessor = null;
|
||||||
|
if (handler != null) {
|
||||||
|
handler.removeCallbacksAndMessages(/* token= */ null);
|
||||||
|
}
|
||||||
|
if (videoEffects != null) {
|
||||||
|
videoEffects.clear();
|
||||||
|
}
|
||||||
|
processedFrames.clear();
|
||||||
|
canEnableFrameProcessing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a maximum video size to use when configuring a codec for {@code format} in a way that
|
* Returns a maximum video size to use when configuring a codec for {@code format} in a way that
|
||||||
* will allow possible adaptation to other compatible formats that are expected to have the same
|
* will allow possible adaptation to other compatible formats that are expected to have the same
|
||||||
|
Loading…
x
Reference in New Issue
Block a user