1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.mediav2.common.cts;
18 
19 import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface;
20 import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible;
21 import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar;
22 import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
23 import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUVP010;
24 
25 import android.media.AudioFormat;
26 import android.media.MediaFormat;
27 
28 import androidx.annotation.NonNull;
29 
30 import java.util.HashMap;
31 import java.util.Map;
32 
33 /**
34  * Class to hold encoder configuration settings.
35  */
36 public class EncoderConfigParams {
37     public static final String TOKEN_SEPARATOR = "<>";
38 
39     public final boolean mIsAudio;
40     public final String mMediaType;
41 
42     // video params
43     public final int mWidth;
44     public final int mHeight;
45     public final int mFrameRate;
46     public final float mKeyFrameInterval;
47     public final int mMaxBFrames;
48     public final int mBitRateMode;
49     public final int mLevel;
50     public final int mColorFormat;
51     public final int mInputBitDepth;
52     public final int mRange;
53     public final int mStandard;
54     public final int mTransfer;
55 
56     // audio params
57     public final int mSampleRate;
58     public final int mChannelCount;
59     public final int mCompressionLevel;
60     public final int mPcmEncoding;
61 
62     // features list
63     public final Map<String, Boolean> mFeatures;
64 
65     // common params
66     public final int mProfile;
67     public final int mBitRate;
68 
69     Builder mBuilder;
70     MediaFormat mFormat;
71     StringBuilder mMsg;
72 
EncoderConfigParams(Builder cfg)73     private EncoderConfigParams(Builder cfg) {
74         if (cfg.mMediaType == null) {
75             throw new IllegalArgumentException("null media type");
76         }
77         mIsAudio = cfg.mMediaType.startsWith("audio/");
78         boolean mIsVideo = cfg.mMediaType.startsWith("video/");
79         if (mIsAudio == mIsVideo) {
80             throw new IllegalArgumentException("invalid media type, it is neither audio nor video");
81         }
82         mMediaType = cfg.mMediaType;
83         if (mIsAudio) {
84             if (cfg.mSampleRate <= 0 || cfg.mChannelCount <= 0) {
85                 throw new IllegalArgumentException("bad config params for audio component");
86             }
87             mSampleRate = cfg.mSampleRate;
88             mChannelCount = cfg.mChannelCount;
89             if (mMediaType.equals(MediaFormat.MIMETYPE_AUDIO_FLAC)) {
90                 if (cfg.mCompressionLevel < 0 || cfg.mCompressionLevel > 8) {
91                     throw new IllegalArgumentException("bad compression level for flac component");
92                 }
93                 mCompressionLevel = cfg.mCompressionLevel;
94                 mBitRate = -1;
95             } else {
96                 if (cfg.mBitRate <= 0) {
97                     throw new IllegalArgumentException("bad bitrate value for audio component");
98                 }
99                 mBitRate = cfg.mBitRate;
100                 mCompressionLevel = -1;
101             }
102             if (cfg.mPcmEncoding != AudioFormat.ENCODING_PCM_FLOAT
103                     && cfg.mPcmEncoding != AudioFormat.ENCODING_PCM_16BIT) {
104                 throw new IllegalArgumentException("bad input pcm encoding for audio component");
105             }
106             if (cfg.mInputBitDepth != -1) {
107                 throw new IllegalArgumentException(
108                         "use pcm encoding to signal input attributes, don't use bitdepth");
109             }
110             mPcmEncoding = cfg.mPcmEncoding;
111             mProfile = cfg.mProfile;
112 
113             // satisfy Variable '*' might not have been initialized, unused by this media type
114             mWidth = 352;
115             mHeight = 288;
116             mFrameRate = -1;
117             mBitRateMode = -1;
118             mKeyFrameInterval = 1.0f;
119             mMaxBFrames = 0;
120             mLevel = -1;
121             mColorFormat = COLOR_FormatYUV420Flexible;
122             mInputBitDepth = -1;
123             mRange = -1;
124             mStandard = -1;
125             mTransfer = -1;
126         } else {
127             if (cfg.mWidth <= 0 || cfg.mHeight <= 0) {
128                 throw new IllegalArgumentException("bad config params for video component");
129             }
130             mWidth = cfg.mWidth;
131             mHeight = cfg.mHeight;
132             if (cfg.mFrameRate <= 0) {
133                 if (mMediaType.equals(MediaFormat.MIMETYPE_VIDEO_MPEG4)) {
134                     mFrameRate = 12;
135                 } else if (mMediaType.equals(MediaFormat.MIMETYPE_VIDEO_H263)) {
136                     mFrameRate = 12;
137                 } else {
138                     mFrameRate = 30;
139                 }
140             } else {
141                 mFrameRate = cfg.mFrameRate;
142             }
143             if (cfg.mBitRate <= 0) {
144                 throw new IllegalArgumentException("bad bitrate value for video component");
145             }
146             mBitRate = cfg.mBitRate;
147             mKeyFrameInterval = cfg.mKeyFrameInterval;
148             mMaxBFrames = cfg.mMaxBFrames;
149             mBitRateMode = cfg.mBitRateMode;
150             mProfile = cfg.mProfile;
151             mLevel = cfg.mLevel;
152             if (cfg.mColorFormat != COLOR_FormatYUV420Flexible
153                     && cfg.mColorFormat != COLOR_FormatYUVP010
154                     && cfg.mColorFormat != COLOR_FormatSurface
155                     && cfg.mColorFormat != COLOR_FormatYUV420SemiPlanar
156                     && cfg.mColorFormat != COLOR_FormatYUV420Planar) {
157                 throw new IllegalArgumentException("bad color format config for video component");
158             }
159             mColorFormat = cfg.mColorFormat;
160             if (cfg.mInputBitDepth != -1) {
161                 if (cfg.mColorFormat == COLOR_FormatYUV420Flexible && cfg.mInputBitDepth != 8) {
162                     throw new IllegalArgumentException(
163                             "bad bit depth configuration for COLOR_FormatYUV420Flexible");
164                 } else if (cfg.mColorFormat == COLOR_FormatYUV420SemiPlanar
165                         && cfg.mInputBitDepth != 8) {
166                     throw new IllegalArgumentException(
167                             "bad bit depth configuration for COLOR_FormatYUV420SemiPlanar");
168                 } else if (cfg.mColorFormat == COLOR_FormatYUV420Planar
169                         && cfg.mInputBitDepth != 8) {
170                     throw new IllegalArgumentException(
171                             "bad bit depth configuration for COLOR_FormatYUV420Planar");
172                 } else if (cfg.mColorFormat == COLOR_FormatYUVP010 && cfg.mInputBitDepth != 10) {
173                     throw new IllegalArgumentException(
174                             "bad bit depth configuration for COLOR_FormatYUVP010");
175                 } else if (cfg.mColorFormat == COLOR_FormatSurface && cfg.mInputBitDepth != 8
176                         && cfg.mInputBitDepth != 10) {
177                     throw new IllegalArgumentException(
178                             "bad bit depth configuration for COLOR_FormatSurface");
179                 }
180                 mInputBitDepth = cfg.mInputBitDepth;
181             } else if (cfg.mColorFormat == COLOR_FormatYUVP010) {
182                 mInputBitDepth = 10;
183             } else {
184                 mInputBitDepth = 8;
185             }
186             if (mProfile == -1) {
187                 if ((mColorFormat == COLOR_FormatSurface && mInputBitDepth == 10) || (mColorFormat
188                         == COLOR_FormatYUVP010)) {
189                     throw new IllegalArgumentException("If color format is configured to "
190                             + "COLOR_FormatSurface and bitdepth is set to 10 or color format is "
191                             + "configured to COLOR_FormatYUVP010 then profile needs to be"
192                             + " configured");
193                 }
194             }
195             mRange = cfg.mRange;
196             mStandard = cfg.mStandard;
197             mTransfer = cfg.mTransfer;
198 
199             // satisfy Variable '*' might not have been initialized, unused by this media type
200             mSampleRate = 8000;
201             mChannelCount = 1;
202             mCompressionLevel = 5;
203             mPcmEncoding = AudioFormat.ENCODING_INVALID;
204         }
205         mFeatures = cfg.mFeatures;
206         mBuilder = cfg;
207     }
208 
getBuilder()209     public Builder getBuilder() throws CloneNotSupportedException {
210         return mBuilder.clone();
211     }
212 
getFormat()213     public MediaFormat getFormat() {
214         if (mFormat != null) return new MediaFormat(mFormat);
215 
216         mFormat = new MediaFormat();
217         mFormat.setString(MediaFormat.KEY_MIME, mMediaType);
218         if (mIsAudio) {
219             mFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, mSampleRate);
220             mFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, mChannelCount);
221             if (mMediaType.equals(MediaFormat.MIMETYPE_AUDIO_FLAC)) {
222                 mFormat.setInteger(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL, mCompressionLevel);
223             } else {
224                 mFormat.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
225             }
226             if (mProfile >= 0 && mMediaType.equals(MediaFormat.MIMETYPE_AUDIO_AAC)) {
227                 mFormat.setInteger(MediaFormat.KEY_PROFILE, mProfile);
228                 mFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, mProfile);
229             }
230             mFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, mPcmEncoding);
231         } else {
232             mFormat.setInteger(MediaFormat.KEY_WIDTH, mWidth);
233             mFormat.setInteger(MediaFormat.KEY_HEIGHT, mHeight);
234             mFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mFrameRate);
235             mFormat.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
236             mFormat.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL, mKeyFrameInterval);
237             mFormat.setInteger(MediaFormat.KEY_MAX_B_FRAMES, mMaxBFrames);
238             if (mBitRateMode >= 0) mFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, mBitRateMode);
239             if (mProfile >= 0) mFormat.setInteger(MediaFormat.KEY_PROFILE, mProfile);
240             if (mLevel >= 0) mFormat.setInteger(MediaFormat.KEY_LEVEL, mLevel);
241             mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat);
242             if (mRange >= 0) mFormat.setInteger(MediaFormat.KEY_COLOR_RANGE, mRange);
243             if (mStandard >= 0) mFormat.setInteger(MediaFormat.KEY_COLOR_STANDARD, mStandard);
244             if (mTransfer >= 0) mFormat.setInteger(MediaFormat.KEY_COLOR_TRANSFER, mTransfer);
245         }
246         for (Map.Entry<String, Boolean> entry : mFeatures.entrySet()) {
247             mFormat.setFeatureEnabled(entry.getKey(), entry.getValue());
248         }
249         return new MediaFormat(mFormat);
250     }
251 
252     /**
253      * Converts MediaFormat object to a string. All Keys, ValueTypes, Values are concatenated with
254      * a separator and sent for further usage.
255      */
serializeMediaFormat(MediaFormat format)256     public static String serializeMediaFormat(MediaFormat format) {
257         StringBuilder msg = new StringBuilder();
258         java.util.Set<String> keys = format.getKeys();
259         for (String key : keys) {
260             int valueTypeForKey = format.getValueTypeForKey(key);
261             msg.append(key).append(TOKEN_SEPARATOR);
262             msg.append(valueTypeForKey).append(TOKEN_SEPARATOR);
263             if (valueTypeForKey == MediaFormat.TYPE_INTEGER) {
264                 msg.append(format.getInteger(key)).append(TOKEN_SEPARATOR);
265             } else if (valueTypeForKey == MediaFormat.TYPE_FLOAT) {
266                 msg.append(format.getFloat(key)).append(TOKEN_SEPARATOR);
267             } else if (valueTypeForKey == MediaFormat.TYPE_STRING) {
268                 msg.append(format.getString(key)).append(TOKEN_SEPARATOR);
269             } else {
270                 throw new RuntimeException("unrecognized Type for Key: " + key);
271             }
272         }
273         return msg.toString();
274     }
275 
276     @NonNull
277     @Override
toString()278     public String toString() {
279         if (mMsg != null) return mMsg.toString();
280 
281         mMsg = new StringBuilder();
282         mMsg.append(String.format("media type : %s, ", mMediaType));
283         if (mIsAudio) {
284             mMsg.append(String.format("Sample rate : %d, ", mSampleRate));
285             mMsg.append(String.format("Channel count : %d, ", mChannelCount));
286             if (mMediaType.equals(MediaFormat.MIMETYPE_AUDIO_FLAC)) {
287                 mMsg.append(String.format("Compression level : %d, ", mCompressionLevel));
288             } else {
289                 mMsg.append(String.format("Bitrate : %d, ", mBitRate));
290             }
291             if (mMediaType.equals(MediaFormat.MIMETYPE_AUDIO_AAC) && mProfile != -1) {
292                 mMsg.append(String.format("Profile : %d, ", mProfile));
293             }
294             mMsg.append(String.format("encoding : %d, ", mPcmEncoding));
295         } else {
296             mMsg.append(String.format("Width : %d, ", mWidth));
297             mMsg.append(String.format("Height : %d, ", mHeight));
298             mMsg.append(String.format("Frame rate : %d, ", mFrameRate));
299             mMsg.append(String.format("Bit rate : %d, ", mBitRate));
300             mMsg.append(String.format("key frame interval : %f, ", mKeyFrameInterval));
301             mMsg.append(String.format("max b frames : %d, ", mMaxBFrames));
302             if (mBitRateMode >= 0) mMsg.append(String.format("bitrate mode : %d, ", mBitRateMode));
303             if (mProfile >= 0) mMsg.append(String.format("profile : %x, ", mProfile));
304             if (mLevel >= 0) mMsg.append(String.format("level : %x, ", mLevel));
305             mMsg.append(String.format("color format : %x, ", mColorFormat));
306             if (mColorFormat == COLOR_FormatSurface) {
307                 mMsg.append(String.format("bit depth : %d, ", mInputBitDepth));
308             }
309             if (mRange >= 0) mMsg.append(String.format("color range : %d, ", mRange));
310             if (mStandard >= 0) mMsg.append(String.format("color standard : %d, ", mStandard));
311             if (mTransfer >= 0) mMsg.append(String.format("color transfer : %d, ", mTransfer));
312         }
313         if (!mFeatures.isEmpty()) {
314             mMsg.append(String.format("features : { "));
315             for (Map.Entry<String, Boolean> entry : mFeatures.entrySet()) {
316                 mMsg.append(String.format(entry.getKey() + " : " + entry.getValue() + ", "));
317             }
318             mMsg.append(String.format("}"));
319         }
320         mMsg.append("\n");
321         return mMsg.toString();
322     }
323 
324     public static class Builder implements Cloneable {
325         public String mMediaType;
326 
327         // video params
328         public int mWidth = 352;
329         public int mHeight = 288;
330         public int mFrameRate = -1;
331         public int mBitRateMode = -1;
332         public float mKeyFrameInterval = 1.0f;
333         public int mMaxBFrames = 0;
334         public int mLevel = -1;
335         public int mColorFormat = COLOR_FormatYUV420Flexible;
336         public int mInputBitDepth = -1;
337         public int mRange = -1;
338         public int mStandard = -1;
339         public int mTransfer = -1;
340 
341         // audio params
342         public int mSampleRate = 8000;
343         public int mChannelCount = 1;
344         public int mCompressionLevel = 5;
345         public int mPcmEncoding = AudioFormat.ENCODING_PCM_16BIT;
346 
347         // feature list
348         public Map<String, Boolean> mFeatures = new HashMap<>();
349 
350         // common params
351         public int mProfile = -1;
352         public int mBitRate = 256000;
353 
Builder(String mediaType)354         public Builder(String mediaType) {
355             mMediaType = mediaType;
356         }
357 
setWidth(int width)358         public Builder setWidth(int width) {
359             this.mWidth = width;
360             return this;
361         }
362 
setHeight(int height)363         public Builder setHeight(int height) {
364             this.mHeight = height;
365             return this;
366         }
367 
setFrameRate(int frameRate)368         public Builder setFrameRate(int frameRate) {
369             this.mFrameRate = frameRate;
370             return this;
371         }
372 
setBitRateMode(int bitRateMode)373         public Builder setBitRateMode(int bitRateMode) {
374             this.mBitRateMode = bitRateMode;
375             return this;
376         }
377 
setKeyFrameInterval(float keyFrameInterval)378         public Builder setKeyFrameInterval(float keyFrameInterval) {
379             this.mKeyFrameInterval = keyFrameInterval;
380             return this;
381         }
382 
setMaxBFrames(int maxBFrames)383         public Builder setMaxBFrames(int maxBFrames) {
384             this.mMaxBFrames = maxBFrames;
385             return this;
386         }
387 
setLevel(int level)388         public Builder setLevel(int level) {
389             this.mLevel = level;
390             return this;
391         }
392 
setColorFormat(int colorFormat)393         public Builder setColorFormat(int colorFormat) {
394             this.mColorFormat = colorFormat;
395             return this;
396         }
397 
setInputBitDepth(int inputBitDepth)398         public Builder setInputBitDepth(int inputBitDepth) {
399             this.mInputBitDepth = inputBitDepth;
400             return this;
401         }
402 
setRange(int range)403         public Builder setRange(int range) {
404             this.mRange = range;
405             return this;
406         }
407 
setStandard(int standard)408         public Builder setStandard(int standard) {
409             this.mStandard = standard;
410             return this;
411         }
412 
setTransfer(int transfer)413         public Builder setTransfer(int transfer) {
414             this.mTransfer = transfer;
415             return this;
416         }
417 
setSampleRate(int sampleRate)418         public Builder setSampleRate(int sampleRate) {
419             this.mSampleRate = sampleRate;
420             return this;
421         }
422 
setChannelCount(int channelCount)423         public Builder setChannelCount(int channelCount) {
424             this.mChannelCount = channelCount;
425             return this;
426         }
427 
setCompressionLevel(int compressionLevel)428         public Builder setCompressionLevel(int compressionLevel) {
429             this.mCompressionLevel = compressionLevel;
430             return this;
431         }
432 
setPcmEncoding(int pcmEncoding)433         public Builder setPcmEncoding(int pcmEncoding) {
434             this.mPcmEncoding = pcmEncoding;
435             return this;
436         }
437 
setProfile(int profile)438         public Builder setProfile(int profile) {
439             this.mProfile = profile;
440             // encoder profile requires also level to be set prior to Android U,
441             // but this can be a default/unknown value. Setting this to 1 as all
442             // codecs use a value of 1 for lowest level.
443             if (mLevel < 0) {
444                 mLevel = 1;
445             }
446             return this;
447         }
448 
setBitRate(int bitRate)449         public Builder setBitRate(int bitRate) {
450             this.mBitRate = bitRate;
451             return this;
452         }
453 
setFeature(String feature, boolean enable)454         public Builder setFeature(String feature, boolean enable) {
455             if (feature != null) {
456                 this.mFeatures.put(feature, enable);
457             }
458             return this;
459         }
460 
build()461         public EncoderConfigParams build() {
462             return new EncoderConfigParams(this);
463         }
464 
465         @NonNull
clone()466         public Builder clone() throws CloneNotSupportedException {
467             Builder builder = (Builder) super.clone();
468             builder.mFeatures.clear();
469             for (Map.Entry<String, Boolean> entry : mFeatures.entrySet()) {
470                 String feature = entry.getKey();
471                 boolean enable = entry.getValue();
472                 builder.mFeatures.put(feature, enable);
473             }
474             return builder;
475         }
476     }
477 }
478