| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "garnet/bin/media/media_service/conversion_pipeline_builder.h" |
| |
| #include "garnet/bin/media/audio/lpcm_reformatter.h" |
| #include "garnet/bin/media/decode/decoder.h" |
| #include "garnet/bin/media/framework/formatting.h" |
| |
| namespace media { |
| |
| namespace { |
| |
| enum class AddResult { |
| kFailed, // Can't convert. |
| kProgressed, // Added a conversion transform. |
| kFinished // Done adding conversion transforms. |
| }; |
| |
| // Produces a score for in_type with respect to out_type_set. The score |
| // is used to compare type sets to see which represents the best goal for |
| // conversion. Higher scores are preferred. A score of zero indicates that |
| // in_type is incompatible with out_type_set. |
| int Score(const AudioStreamType& in_type, |
| const AudioStreamTypeSet& out_type_set) { |
| // TODO(dalesat): Plenty of room for more subtlety here. Maybe actually |
| // measure conversion costs (cpu, quality, etc) and reflect them here. |
| |
| int score = 1; // We can convert anything, so 1 is the minimum score. |
| |
| if (in_type.sample_format() == out_type_set.sample_format() || |
| out_type_set.sample_format() == AudioStreamType::SampleFormat::kAny) { |
| // Prefer not to convert sample format. |
| score += 10; |
| } else { |
| // Prefer higher-quality formats. |
| switch (out_type_set.sample_format()) { |
| case AudioStreamType::SampleFormat::kUnsigned8: |
| break; |
| case AudioStreamType::SampleFormat::kSigned16: |
| score += 1; |
| break; |
| case AudioStreamType::SampleFormat::kSigned24In32: |
| score += 2; |
| break; |
| case AudioStreamType::SampleFormat::kFloat: |
| score += 3; |
| break; |
| default: |
| FXL_DCHECK(false) << "unsupported sample format " |
| << out_type_set.sample_format(); |
| } |
| } |
| |
| if (out_type_set.channels().contains(in_type.channels())) { |
| // Prefer not to mixdown/up. |
| score += 10; |
| } else { |
| return 0; // TODO(dalesat): Remove when we have mixdown/up. |
| } |
| |
| if (out_type_set.frames_per_second().contains(in_type.frames_per_second())) { |
| // Very much prefer not to resample. |
| score += 50; |
| } else { |
| return 0; // TODO(dalesat): Remove when we have resamplers. |
| } |
| |
| return score; |
| } |
| |
| // Finds the stream type set that best matches in_type. |
| const std::unique_ptr<StreamTypeSet>* FindBestLpcm( |
| const AudioStreamType& in_type, |
| const std::vector<std::unique_ptr<StreamTypeSet>>& out_type_sets) { |
| const std::unique_ptr<StreamTypeSet>* best = nullptr; |
| int best_score = 0; |
| |
| for (const std::unique_ptr<StreamTypeSet>& out_type_set : out_type_sets) { |
| if (out_type_set->medium() == StreamType::Medium::kAudio && |
| out_type_set->IncludesEncoding(StreamType::kAudioEncodingLpcm)) { |
| int score = Score(in_type, *out_type_set->audio()); |
| if (best_score < score) { |
| best_score = score; |
| best = &out_type_set; |
| } |
| } |
| } |
| |
| return best; |
| } |
| |
| // Attempts to add transforms to the pipeline given an input compressed audio |
| // stream type with (in_type) and the set of output types we need to convert to |
| // (out_type_sets). If the call succeeds, *out_type is set to the new output |
| // type. Otherwise, *out_type is set to nullptr. |
| AddResult AddTransformsForCompressedAudio( |
| const AudioStreamType& in_type, |
| const std::vector<std::unique_ptr<StreamTypeSet>>& out_type_sets, |
| Graph* graph, |
| OutputRef* output, |
| std::unique_ptr<StreamType>* out_type) { |
| FXL_DCHECK(out_type); |
| FXL_DCHECK(graph); |
| |
| // See if we have a matching audio type. |
| for (const std::unique_ptr<StreamTypeSet>& out_type_set : out_type_sets) { |
| if (out_type_set->medium() == StreamType::Medium::kAudio) { |
| if (out_type_set->audio()->Includes(in_type)) { |
| // No transform needed. |
| *out_type = in_type.Clone(); |
| return AddResult::kFinished; |
| } |
| // TODO(dalesat): Support a different compressed output type by |
| // transcoding. |
| } |
| } |
| |
| // Find the best LPCM output type. |
| const std::unique_ptr<StreamTypeSet>* best = |
| FindBestLpcm(in_type, out_type_sets); |
| if (best == nullptr) { |
| // No candidates found. |
| *out_type = nullptr; |
| return AddResult::kFailed; |
| } |
| |
| FXL_DCHECK((*best)->medium() == StreamType::Medium::kAudio); |
| FXL_DCHECK((*best)->IncludesEncoding(StreamType::kAudioEncodingLpcm)); |
| |
| // Need to decode. Create a decoder and go from there. |
| std::shared_ptr<Decoder> decoder; |
| Result result = Decoder::Create(in_type, &decoder); |
| if (result != Result::kOk) { |
| // No decoder found. |
| *out_type = nullptr; |
| return AddResult::kFailed; |
| } |
| |
| *output = graph->ConnectOutputToNode(*output, graph->Add(decoder)).output(); |
| *out_type = decoder->output_stream_type(); |
| |
| return AddResult::kProgressed; |
| } |
| |
| // Attempts to add transforms to the pipeline given an input compressed video |
| // stream type with (in_type) and the set of output types we need to convert to |
| // (out_type_sets). If the call succeeds, *out_type is set to the new output |
| // type. Otherwise, *out_type is set to nullptr. |
| AddResult AddTransformsForCompressedVideo( |
| const VideoStreamType& in_type, |
| const std::vector<std::unique_ptr<StreamTypeSet>>& out_type_sets, |
| Graph* graph, |
| OutputRef* output, |
| std::unique_ptr<StreamType>* out_type) { |
| FXL_DCHECK(out_type); |
| FXL_DCHECK(graph); |
| |
| // TODO(dalesat): See if we already have a matching video type. |
| |
| // Need to decode. Create a decoder and go from there. |
| std::shared_ptr<Decoder> decoder; |
| Result result = Decoder::Create(in_type, &decoder); |
| if (result != Result::kOk) { |
| // No decoder found. |
| *out_type = nullptr; |
| return AddResult::kFailed; |
| } |
| |
| *output = graph->ConnectOutputToNode(*output, graph->Add(decoder)).output(); |
| *out_type = decoder->output_stream_type(); |
| |
| return AddResult::kProgressed; |
| } |
| |
| // Attempts to add transforms to the pipeline given an input LPCM stream type |
| // (in_type) and the output lpcm stream type set for the type we need to |
| // convert to (out_type_set). If the call succeeds, *out_type is set to the new |
| // output type. Otherwise, *out_type is set to nullptr. |
| AddResult AddTransformsForLpcm(const AudioStreamType& in_type, |
| const AudioStreamTypeSet& out_type_set, |
| Graph* graph, |
| OutputRef* output, |
| std::unique_ptr<StreamType>* out_type) { |
| FXL_DCHECK(graph); |
| FXL_DCHECK(out_type); |
| |
| // TODO(dalesat): Room for more intelligence here wrt transform ordering and |
| // transforms that handle more than one conversion. |
| if (in_type.sample_format() != out_type_set.sample_format() && |
| out_type_set.sample_format() != AudioStreamType::SampleFormat::kAny) { |
| *output = graph |
| ->ConnectOutputToNode( |
| *output, graph->Add(LpcmReformatter::Create( |
| in_type, out_type_set.sample_format()))) |
| .output(); |
| } |
| |
| if (!out_type_set.channels().contains(in_type.channels())) { |
| // TODO(dalesat): Insert mixdown/up transform. |
| FXL_DCHECK(false) << "conversion requires mixdown/up - not supported"; |
| *out_type = nullptr; |
| return AddResult::kFailed; |
| } |
| |
| if (!out_type_set.frames_per_second().contains(in_type.frames_per_second())) { |
| // TODO(dalesat): Insert resampler. |
| FXL_DCHECK(false) << "conversion requires resampling - not supported"; |
| *out_type = nullptr; |
| return AddResult::kFailed; |
| } |
| |
| // Build the resulting media type. |
| *out_type = AudioStreamType::Create( |
| StreamType::kAudioEncodingLpcm, nullptr, |
| out_type_set.sample_format() == AudioStreamType::SampleFormat::kAny |
| ? in_type.sample_format() |
| : out_type_set.sample_format(), |
| in_type.channels(), in_type.frames_per_second()); |
| |
| return AddResult::kFinished; |
| } |
| |
| // Attempts to add transforms to the pipeline given an input audio stream type |
| // witn lpcm encoding (in_type) and the set of output types we need to convert |
| // to (out_type_sets). If the call succeeds, *out_type is set to the new |
| // output type. Otherwise, *out_type is set to nullptr. |
| AddResult AddTransformsForLpcm( |
| const AudioStreamType& in_type, |
| const std::vector<std::unique_ptr<StreamTypeSet>>& out_type_sets, |
| Graph* graph, |
| OutputRef* output, |
| std::unique_ptr<StreamType>* out_type) { |
| FXL_DCHECK(graph); |
| FXL_DCHECK(out_type); |
| |
| const std::unique_ptr<StreamTypeSet>* best = |
| FindBestLpcm(in_type, out_type_sets); |
| if (best == nullptr) { |
| // TODO(dalesat): Support a compressed output type by encoding. |
| FXL_DCHECK(false) << "conversion using encoder not supported"; |
| *out_type = nullptr; |
| return AddResult::kFailed; |
| } |
| |
| FXL_DCHECK((*best)->medium() == StreamType::Medium::kAudio); |
| |
| return AddTransformsForLpcm(in_type, *(*best)->audio(), graph, output, |
| out_type); |
| } |
| |
| // Attempts to add transforms to the pipeline given an input media type of any |
| // medium and encoding (in_type) and the set of output types we need to |
| // convert to (out_type_sets). If the call succeeds, *out_type is set to the new |
| // output type. Otherwise, *out_type is set to nullptr. |
| AddResult AddTransforms( |
| const StreamType& in_type, |
| const std::vector<std::unique_ptr<StreamTypeSet>>& out_type_sets, |
| Graph* graph, |
| OutputRef* output, |
| std::unique_ptr<StreamType>* out_type) { |
| FXL_DCHECK(graph); |
| FXL_DCHECK(out_type); |
| |
| switch (in_type.medium()) { |
| case StreamType::Medium::kAudio: |
| if (in_type.encoding() == StreamType::kAudioEncodingLpcm) { |
| return AddTransformsForLpcm(*in_type.audio(), out_type_sets, graph, |
| output, out_type); |
| } else { |
| return AddTransformsForCompressedAudio(*in_type.audio(), out_type_sets, |
| graph, output, out_type); |
| } |
| case StreamType::Medium::kVideo: |
| if (in_type.encoding() == StreamType::kVideoEncodingUncompressed) { |
| *out_type = in_type.Clone(); |
| return AddResult::kFinished; |
| } else { |
| return AddTransformsForCompressedVideo(*in_type.video(), out_type_sets, |
| graph, output, out_type); |
| } |
| default: |
| FXL_DCHECK(false) << "conversion not supported for medium" |
| << in_type.medium(); |
| *out_type = nullptr; |
| return AddResult::kFailed; |
| } |
| } |
| |
| } // namespace |
| |
| bool BuildConversionPipeline( |
| const StreamType& in_type, |
| const std::vector<std::unique_ptr<StreamTypeSet>>& out_type_sets, |
| Graph* graph, |
| OutputRef* output, |
| std::unique_ptr<StreamType>* out_type) { |
| FXL_DCHECK(graph); |
| FXL_DCHECK(output); |
| FXL_DCHECK(out_type); |
| |
| OutputRef out = *output; |
| const StreamType* type_to_convert = &in_type; |
| std::unique_ptr<StreamType> converted_type; |
| while (true) { |
| switch (AddTransforms(*type_to_convert, out_type_sets, graph, &out, |
| &converted_type)) { |
| case AddResult::kFailed: |
| // Failed to find a suitable conversion. Return the pipeline to its |
| // original state. |
| graph->RemoveNodesConnectedToOutput(*output); |
| *out_type = nullptr; |
| return false; |
| case AddResult::kProgressed: |
| // Made progress. Continue. |
| break; |
| case AddResult::kFinished: |
| // No further conversion required. |
| *output = out; |
| *out_type = std::move(converted_type); |
| return true; |
| } |
| |
| type_to_convert = converted_type.get(); |
| } |
| } |
| |
| } // namespace media |