• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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