1 /* 2 * Copyright 2024 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 com.android.photopicker.features.preparemedia 18 19 import android.content.Context 20 import android.media.ApplicationMediaCapabilities 21 import android.media.MediaFeature.HdrType 22 import android.media.MediaFormat 23 import android.media.MediaFormat.COLOR_STANDARD_BT2020 24 import android.media.MediaFormat.COLOR_STANDARD_BT709 25 import android.media.MediaFormat.COLOR_TRANSFER_HLG 26 import android.media.MediaFormat.COLOR_TRANSFER_ST2084 27 import android.media.MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION 28 import android.media.MediaFormat.MIMETYPE_VIDEO_HEVC 29 import android.util.Log 30 import androidx.annotation.VisibleForTesting 31 import androidx.media3.common.util.MediaFormatUtil.createFormatFromMediaFormat 32 import androidx.media3.common.util.MediaFormatUtil.isVideoFormat 33 import androidx.media3.exoplayer.MediaExtractorCompat 34 import com.android.photopicker.core.events.Telemetry 35 import com.android.photopicker.data.model.Media 36 37 /** A class that help video transcode. */ 38 class TranscoderImpl : Transcoder { 39 40 private var needsTranscodingVideoInfo: Transcoder.VideoInfo? = null 41 isTranscodeRequirednull42 override fun isTranscodeRequired( 43 context: Context, 44 mediaCapabilities: ApplicationMediaCapabilities?, 45 video: Media.Video, 46 ): Boolean { 47 if (mediaCapabilities == null) { 48 return false 49 } 50 51 if (video.duration > DURATION_LIMIT_MS) { 52 Log.w(TAG, "Duration (${video.duration} ms) is over limit ($DURATION_LIMIT_MS).") 53 return false 54 } 55 56 // Check if any video tracks need to be transcoded. 57 val videoTrackMediaFormats = getVideoTrackMediaFormats(context, video) 58 for (mediaFormat in videoTrackMediaFormats) { 59 if (isTranscodeRequired(mediaFormat, mediaCapabilities, video.duration)) { 60 return true 61 } 62 } 63 64 return false 65 } 66 67 /** 68 * Gets the [MediaFormat]s of the video tracks in the given video. 69 * 70 * @param context The context. 71 * @param video The video to check. 72 * @return The [MediaFormat]s of the video tracks in the given video. 73 */ getVideoTrackMediaFormatsnull74 private fun getVideoTrackMediaFormats(context: Context, video: Media.Video): List<MediaFormat> { 75 val mediaFormats = mutableListOf<MediaFormat>() 76 77 try { 78 val extractor = MediaExtractorCompat(context) 79 extractor.setDataSource(video.mediaUri, 0) 80 81 for (index in 0..<extractor.trackCount) { 82 val mediaFormat = extractor.getTrackFormat(index) 83 if (isVideoFormat(mediaFormat)) { 84 mediaFormats.add(mediaFormat) 85 } 86 } 87 } catch (e: Exception) { 88 Log.e(TAG, "Failed to get MediaFormat of URI (${video.mediaUri}).", e) 89 } 90 91 return mediaFormats 92 } 93 94 /** 95 * Checks if a transcode is required for the given [MediaFormat]. 96 * 97 * @param mediaFormat The [MediaFormat] to check. 98 * @return True if a transcode is required for the given [MediaFormat], false otherwise. 99 */ 100 @VisibleForTesting isTranscodeRequirednull101 fun isTranscodeRequired( 102 mediaFormat: MediaFormat, 103 mediaCapabilities: ApplicationMediaCapabilities, 104 duration: Int = 0, 105 ): Boolean { 106 val format = createFormatFromMediaFormat(mediaFormat) 107 val mimeType = format.sampleMimeType 108 val colorStandard = format.colorInfo?.colorSpace 109 val colorTransfer = format.colorInfo?.colorTransfer 110 111 with(mediaCapabilities) { 112 if (isHevc(mimeType)) { 113 // Not to transcode when App does not support HEVC, as it is indistinguishable 114 // from the 'unset' case. Transcode for "HEVC -> AVC for SDR content" might not be 115 // what the caller intended. 116 117 if (isHlg10(colorStandard, colorTransfer) && !isHdrTypeSupported(HdrType.HLG)) { 118 needsTranscodingVideoInfo = 119 Transcoder.VideoInfo( 120 duration, 121 colorStandard ?: 0, 122 colorTransfer ?: 0, 123 Telemetry.VideoMimeType.HEVC.type, 124 ) 125 return true 126 } 127 128 if ( 129 isHdr10OrHdr10Plus(colorStandard, colorTransfer) && 130 (!isHdrTypeSupported(HdrType.HDR10) || 131 !isHdrTypeSupported(HdrType.HDR10_PLUS)) 132 ) { 133 needsTranscodingVideoInfo = 134 Transcoder.VideoInfo( 135 duration, 136 colorStandard ?: 0, 137 colorTransfer ?: 0, 138 Telemetry.VideoMimeType.HEVC.type, 139 ) 140 return true 141 } 142 } 143 144 if ( 145 isHdrDolbyVision(mimeType, colorStandard, colorTransfer) && 146 !isHdrTypeSupported(HdrType.DOLBY_VISION) 147 ) { 148 needsTranscodingVideoInfo = 149 Transcoder.VideoInfo( 150 duration, 151 colorStandard ?: 0, 152 colorTransfer ?: 0, 153 Telemetry.VideoMimeType.DOLBY.type, 154 ) 155 return true 156 } 157 } 158 return false 159 } 160 161 /** Returns details of the video that needs transcoding */ getTranscodingVideoInfonull162 override fun getTranscodingVideoInfo(): Transcoder.VideoInfo? { 163 return needsTranscodingVideoInfo 164 } 165 166 companion object { 167 private const val TAG = "Transcoder" 168 @VisibleForTesting const val DURATION_LIMIT_MS = 60_000L // 1 min 169 170 /** 171 * Checks if the mime type is HEVC. 172 * 173 * @param mimeType The mime type. 174 * @return True if the mime type is HEVC, false otherwise. 175 */ isHevcnull176 private fun isHevc(mimeType: String?): Boolean { 177 return MIMETYPE_VIDEO_HEVC.equals(mimeType, ignoreCase = true) 178 } 179 180 /** 181 * Checks if the given parameters represent HLG. 182 * 183 * @param colorStandard The color standard. 184 * @param colorTransfer The color transfer. 185 * @return True if the parameters represent HLG, false otherwise. 186 */ isHlg10null187 private fun isHlg10(colorStandard: Int?, colorTransfer: Int?): Boolean { 188 return (colorStandard == COLOR_STANDARD_BT709 || 189 colorStandard == COLOR_STANDARD_BT2020) && colorTransfer == COLOR_TRANSFER_HLG 190 } 191 192 /** 193 * Checks if the given parameters represent HDR10 or HDR10+. 194 * 195 * @param colorStandard The color standard. 196 * @param colorTransfer The color transfer. 197 * @return True if the parameters represent HDR10 or HDR10+, false otherwise. 198 */ isHdr10OrHdr10Plusnull199 private fun isHdr10OrHdr10Plus(colorStandard: Int?, colorTransfer: Int?): Boolean { 200 return colorStandard == COLOR_STANDARD_BT2020 && colorTransfer == COLOR_TRANSFER_ST2084 201 } 202 203 /** 204 * Checks if the given parameters represent HDR Dolby Vision. 205 * 206 * @param mimeType The mime type. 207 * @param colorStandard The color standard. 208 * @param colorTransfer The color transfer. 209 * @return True if the parameters represent HDR Dolby Vision, false otherwise. 210 */ isHdrDolbyVisionnull211 private fun isHdrDolbyVision( 212 mimeType: String?, 213 colorStandard: Int?, 214 colorTransfer: Int?, 215 ): Boolean { 216 return (MIMETYPE_VIDEO_DOLBY_VISION.equals(mimeType, ignoreCase = true)) && 217 COLOR_STANDARD_BT2020 == colorStandard && 218 (colorTransfer == COLOR_TRANSFER_ST2084 || colorTransfer == COLOR_TRANSFER_HLG) 219 } 220 } 221 } 222