public static decimal GetDurationFromFfmpegLogOrMp3File(string logText, string filePath) { decimal result; var parsed = GetDurationFromFfmpegLog(logText, out result); if (!parsed) { // Try to determine the duration directly by reading the file. using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { result = RecordingUtils.GetMp3Duration(stream); } } return(result); }
public async Task <RecordingDetails> Transcode(IEnumerable <KeyValuePair <string, MemoryStream> > tracks, string userIdKey, string timeKey, string extension) { RecordingDetails result = null; // ffmpeg.exe works with files. Produce file paths. var workDirPath = GeneralUtils.GetAppDataDir(); var fileNamePrefix = userIdKey + "_" + timeKey; var trackItems = tracks .Select(i => new TrackItem { Name = i.Key, Stream = i.Value, OriginalFile = Path.Combine(workDirPath, String.Format("{0}_{1}.{2}", fileNamePrefix, i.Key, extension)), IntermidiateFile = Path.Combine(workDirPath, String.Format("{0}_{1}_intermidiate.mp3", fileNamePrefix, i.Key)), }) .ToList(); var inputListFilePath = Path.Combine(workDirPath, fileNamePrefix + ".txt"); var outputFilePath = Path.ChangeExtension(inputListFilePath, ".mp3"); const string resultFileName = "result"; var outputBlobName = ExerciseUtils.FormatBlobName(userIdKey, timeKey, resultFileName, "mp3"); var logBlobName = ExerciseUtils.FormatBlobName(userIdKey, timeKey, resultFileName, "log"); try { // Save the original tracks to the disk. foreach (var i in trackItems) { using (FileStream stream = new FileStream(i.OriginalFile, FileMode.Create, FileAccess.Write)) { i.Stream.WriteTo(stream); } } // Convert to MP3. // ffmpeg fails to concatenate AMRs, the error text is misleading "mylistfile.txt: Input/output error". We convert each file separately, then concatenate MP3s. foreach (var i in trackItems) { // Increase audio volume by 10dB, convert to MP3 CBR 32kbit/s. var arguments = String.Format("-i \"{0}\" -af \"volume=10dB\" -b:a 32k \"{1}\"", i.OriginalFile, i.IntermidiateFile); i.Log = RecordingUtils.RunFfmpeg(arguments); } // Pass the file names to concatenate to ffmpeg.exe in a text file. var inputListLines = trackItems.Select(i => String.Format("file '{0}'", i.IntermidiateFile)); File.WriteAllLines(inputListFilePath, inputListLines); // Concatenate MP3s. Do not re-encode, copy existing streams as is. var concatArguments = String.Format("-f concat -i \"{0}\" -c copy \"{1}\"", inputListFilePath, outputFilePath); var resultLog = RecordingUtils.RunFfmpeg(concatArguments); var separator = Environment.NewLine + "----------------------------------------" + Environment.NewLine; var logText = String.Join(separator, trackItems.Select(i => i.Log)) + separator + String.Join(Environment.NewLine, inputListLines) + separator + resultLog; var containerName = AzureStorageUtils.ContainerNames.Artifacts; var taskMp3 = AzureStorageUtils.UploadFromFileAsync(outputFilePath, containerName, outputBlobName, "audio/mpeg"); var taskLog = AzureStorageUtils.UploadTextAsync(logText, containerName, logBlobName, "text/plain"); // Upload the blobs simultaneously. await Task.WhenAll(taskMp3, taskLog); // Get the recording durations. var trackDurations = trackItems .Select((i) => { var trackDuration = RecordingUtils.GetDurationFromFfmpegLogOrMp3File(i.Log, i.IntermidiateFile); return(new KeyValuePair <string, decimal>(i.Name, trackDuration)); }) .ToDictionary(i => i.Key, i => i.Value) ; var duration = RecordingUtils.GetDurationFromFfmpegLogOrMp3File(resultLog, outputFilePath); // The JSON encoder with default settings doesn't make upper-case -> lower-case letter conversion of property names. The receiving side is case-sensitive. result = new RecordingDetails { BlobName = outputBlobName, TotalDuration = duration, TrackDurations = trackDurations, }; } finally { // Clean up the local disk. foreach (var i in trackItems) { File.Delete(i.OriginalFile); File.Delete(i.IntermidiateFile); } File.Delete(inputListFilePath); File.Delete(outputFilePath); } return(result); }