public async Task <IHttpActionResult> GetFromFreeswitch(string uuid, int userId = 0) { RecordingDetails result = null; // Produce file paths. var workDirPath = GeneralUtils.GetAppDataDir(); var sourceFilePath = Path.Combine(workDirPath, uuid + ".wav"); var outputFilePath = Path.ChangeExtension(sourceFilePath, "mp3"); // The path structure in the Blob Storage is userIdKey/timeKey/filename.ext var userIdKey = KeyUtils.IntToKey(userId); var timeKey = KeyUtils.GetTimeAsBase32(); var outputBlobName = String.Format("{0}/{1}/{2}.mp3", userIdKey, timeKey, uuid); var logBlobName = Path.ChangeExtension(outputBlobName, "log"); try { // Convert to MP3. Increase the audio volume by 10dB, convert to MP3 CBR 64kbit/s. var arguments = String.Format("-i \"{0}\" -af \"volume=10dB\" -b:a 64k \"{1}\"", sourceFilePath, outputFilePath); var logText = RecordingUtils.RunFfmpeg(arguments); 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's duration. var duration = RecordingUtils.GetDurationFromFfmpegLogOrMp3File(logText, outputFilePath); // Delete the original WAV file on success. File.Delete(sourceFilePath); // 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, }; } finally { // Clean up the MP3 file anyway. File.Delete(outputFilePath); } return(Ok <RecordingDetails>(result)); }
public async Task <IHttpActionResult> PostTracks(string metadataName) { //~~ Validate. if (!Request.Content.IsMimeMultipartContent()) { return(BadRequest()); } // Parse the request body parts. var streamProvider = await Request.Content.ReadAsMultipartAsync(); // Check mediaType of the files var mediaTypes = streamProvider.Contents .Select(i => i.Headers) .Where(i => i.ContentDisposition.Name.Trim('"') != metadataName) .Select(i => i.ContentType) //.Where(i => i != null) // ContentType is null in the 'metadata' part. .Select(i => i.MediaType) .Distinct() ; var mediaType = mediaTypes.FirstOrDefault(); var acceptedMediaTypes = new[] { MediaType.Mpeg, MediaType.Mp3, MediaType.Amr, MediaType.Gpp, MediaType.QuickTime }; if ((mediaTypes.Count() != 1) || !acceptedMediaTypes.Contains(mediaType)) { return(BadRequest(String.Join(", ", mediaTypes))); } //~~ Save the original media files to blobs. var userId = this.GetUserId(); // May be 0 if the user is unauthenticated var userIdKey = KeyUtils.IntToKey(userId); var timeKey = KeyUtils.GetTimeAsBase32(); // The length of extId is 12 chars. var extId = (userId != 0) ? null : this.GetExtId(); var longTimeKey = timeKey + extId; // If an operand of string concatenation is null, an empty string is substituted. var extension = MediaType.GetExtension(mediaType); var tracks = new List <KeyValuePair <string, MemoryStream> >(); var fileNames = new List <string>(); foreach (var content in streamProvider.Contents) { var name = content.Headers.ContentDisposition.Name.Trim('"'); var contentType = content.Headers.ContentType; // We have checked above, if ContentType has a value, it has the right MediaType. if ((name != metadataName) && (contentType != null)) { fileNames.Add(name); var stream = await content.ReadAsStreamAsync(); using (stream) { var memStream = new MemoryStream(); await stream.CopyToAsync(memStream); var pair = new KeyValuePair <string, MemoryStream>(name, memStream); tracks.Add(pair); } } } // Upload all blobs in parallel. var tasks = tracks.Select(i => { var blobName = ExerciseUtils.FormatBlobName(userIdKey, longTimeKey, i.Key, extension); return(AzureStorageUtils.UploadBlobAsync(i.Value, AzureStorageUtils.ContainerNames.Artifacts, blobName, mediaType)); }); await Task.WhenAll(tasks); //~~ Call the transcoding service. ///* We send the file names as a comma separated list. There is also a binding in the Web API like this: //public IHttpActionResult GetFoo([FromUri] int[] ids); Call: /Foo?ids=1&ids=2&ids=3 or /Foo?ids[0]=1&ids[1]=2&ids[2]=3 //public IHttpActionResult GetFoo([FromUri] List<string> ids); Call: /Foo?ids[]="a"&ids[]="b"&ids[]="c" //*/ //var host = ConfigurationManager.AppSettings["RecorderHost"]; //var urlFormat = "http://{0}/api/recordings/transcoded/?userIdKey={1}&timeKey={2}&extension={3}&fileNames={4}"; //var url = String.Format(urlFormat, host, userIdKey, longTimeKey, extension, String.Join(",", fileNames)); //HttpClient client = new HttpClient(); //HttpResponseMessage response = await client.GetAsync(url); //if (!response.IsSuccessStatusCode) //{ // // return RedirectToAction(referrerAction, new { error = "transcoding_error" }); // return InternalServerError(new Exception("Transcoding error. " + response.StatusCode.ToString())); //} //// Error is returned as HTML. Then we get error here: No MediaTypeFormatter is available to read an object of type 'JObject' from content with media type 'text/html'. //var recordingDetails = await response.Content.ReadAsAsync<RecordingDetails>(); //// Make sure the duration is known. If the transcoder has failed to parse the ffmpeg logs, it returns DurationMsec = 0. //if (recordingDetails.TotalDuration == 0) //{ // // Read the blob and try to determine the duration directly. // recordingDetails.TotalDuration = // await RecordingUtils.GetMp3Duration(AzureStorageUtils.ContainerNames.Artifacts, recordingDetails.BlobName); //} var transcoder = new RecordingTranscoder(); var recordingDetails = await transcoder.Transcode(tracks, userIdKey, longTimeKey, extension); // Release the memory streams. tracks.ForEach(i => { i.Value.Dispose(); }); //~~ Read the metadata. // Chrome wraps the part name in double-quotes. var metadataContent = streamProvider.Contents .Single(i => i.Headers.ContentDisposition.Name.Trim('"') == metadataName) ; var metadataJson = await metadataContent.ReadAsStringAsync(); var metadata = JObject.Parse(metadataJson); var serviceType = (string)metadata["serviceType"]; var cardId = (Guid?)metadata["cardId"]; var title = (string)metadata["title"]; var comment = (string)metadata["comment"]; var serializer = new JsonSerializer() { ContractResolver = new CamelCasePropertyNamesContractResolver() }; var details = JObject.FromObject(recordingDetails, serializer); if (userId != 0) { //~~ Create a database record. var exerciseId = await ExerciseUtils.CreateExercise(recordingDetails.BlobName, userId, serviceType, ArtifactType.Mp3, recordingDetails.TotalDuration, title, cardId, comment, details.ToString(Formatting.None)); //~~ The client will redirect to the View exercise page. return(Ok(new { ExerciseId = exerciseId })); } else { //~~ Save the details for future use. metadata.Add(new JProperty("recordingDetails", details)); var blobName = ExerciseUtils.FormatBlobName(userIdKey, longTimeKey, "metadata", "json"); await AzureStorageUtils.UploadTextAsync(metadata.ToString(), AzureStorageUtils.ContainerNames.Artifacts, blobName, MediaType.Json); //~~ The client will redirect to the Signup page. longTimeKey will be built from timeKey on the Claim page. return(Ok(new { Key = timeKey })); } }
public async Task <ActionResult> SaveRecordingMobile(HttpPostedFileBase fileInput, string recorderId, string recordingTitle) { /* * 1. Save the original media file to a blob * 2. Call the remote Transcoding service. Pass the blob name. * 3. The Transcoding service saves the MP3 file to a blob and returns its name and the recording's duration. * 4. Create a database record. */ if (fileInput == null) { return(RedirectToAction("RecordSpeech", new { error = "no_file" })); } // return RedirectToAction("RecordSpeech", new { error = "test01" }); var contentType = fileInput.ContentType; var acceptedContentType = (new[] { MediaType.Amr, MediaType.Gpp, MediaType.QuickTime }).Contains(contentType); if (!acceptedContentType) { return(RedirectToAction("RecordSpeech", new { error = contentType })); } var userId = this.GetUserId(); // 1. Save the original file. // The directory structure in Blob Storage is userIdKey/timeKey/originalFileName.ext var timeKey = KeyUtils.GetTimeAsBase32(); var fileName = fileInput.FileName; // Sanitize the fileName. Reserved URL characters must be properly escaped. fileName = String.IsNullOrWhiteSpace(fileName) ? timeKey : Uri.EscapeUriString(fileName.Trim()); var invalidChars = Path.GetInvalidFileNameChars(); fileName = new String(fileName.Select(i => invalidChars.Contains(i) ? '_' : i).ToArray()); if (!Path.HasExtension(fileName)) { Path.ChangeExtension(fileName, MediaType.GetExtension(contentType)); } var blobName = KeyUtils.IntToKey(userId) + AzureStorageUtils.DefaultDirectoryDelimiter + timeKey + AzureStorageUtils.DefaultDirectoryDelimiter + fileName; using (var stream = fileInput.InputStream) { await AzureStorageUtils.UploadBlobAsync(stream, AzureStorageUtils.ContainerNames.Recordings, blobName, contentType); } // 2. Call the transcoding service. var host = ConfigurationManager.AppSettings["RecorderHost"]; var url = String.Format("http://{0}/api/recordings/transcoded/?inputBlobName={1}", host, blobName); HttpClient client = new HttpClient(); HttpResponseMessage response = await client.GetAsync(url); if (!response.IsSuccessStatusCode) { return(RedirectToAction("RecordSpeech", new { error = "transcoding_error" })); } // 3. Get the results from the service. // Error is returned as HTML. Then we get error here: No MediaTypeFormatter is available to read an object of type 'JObject' from content with media type 'text/html'. var value = await response.Content.ReadAsAsync <JObject>(); var outputBlobName = (string)value["outputBlobName"]; var duration = Convert.ToDecimal((int)value["durationMsec"] / 1000.0m); // The transcoder may return -1 if it has failed to parse the ffmpeg logs. if (duration < 0) { // Read the blob and try to determine the duration directly. duration = await RecordingUtils.GetMp3Duration(AzureStorageUtils.ContainerNames.Recordings, outputBlobName); } // 4. Create a database record. var exerciseId = await ExerciseUtils.CreateExercise(outputBlobName, userId, ServiceType.IeltsSpeaking, ArtifactType.Mp3, duration, recordingTitle); // 5. Redirect to the exercise page. return(RedirectToAction("View", new { Id = exerciseId })); }
public async Task <ActionResult> SavePhotos(IEnumerable <HttpPostedFileBase> files, IEnumerable <int> rotations, string serviceType, Guid?cardId, string title, string comment) { // Find out the action to redirect to on error. We use the referrer string. RedirectToAction seems accept a case-insensitive parameter. var referrerAction = (Request.UrlReferrer.Segments.Skip(2).Take(1).SingleOrDefault() ?? "Index").Trim('/'); //referrerAction = referrerAction.First().ToString().ToUpper() + referrerAction.Substring(1).ToLower(); // var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext)); if (files == null || files.All(i => i == null)) { return(RedirectToAction(referrerAction, new { error = "no_file" })); } if (files.Count() != rotations.Count()) { return(RedirectToAction(referrerAction, new { error = "wrong_rotations" })); } var items = files .Select((i, idx) => new { File = i, Rotation = rotations.ElementAt(idx) }) // There may be empty form parts from input elements with no file selected. .Where(i => i.File != null); if (!items.All(i => i.File.ContentType == MediaType.Jpeg)) { return(RedirectToAction(referrerAction, new { error = "wrong_file_format" })); } var userId = this.GetUserId(); var userKey = KeyUtils.IntToKey(userId); var timeKey = KeyUtils.GetTimeAsBase32(); var page = 1; // Pages start counting from 1. var blobNames = new List <string>(); foreach (var item in items) { using (var inputStream = item.File.InputStream) { var blobName = String.Format("{0}/{1}/{2}.original.jpg", userKey, timeKey, page); await AzureStorageUtils.UploadBlobAsync(inputStream, AzureStorageUtils.ContainerNames.Artifacts, blobName, MediaType.Jpeg); using (var memoryStream = new MemoryStream()) { /* I am not sure about using JpegBitmapDecoder. Because of the native code dependencies, the PresentationCore and WindowsBase assemblies need to be distributed as x86 and x64, so AnyCPU may be not possible? */ inputStream.Seek(0, SeekOrigin.Begin); using (var image = Image.FromStream(inputStream)) { RotateFlipType rotateFlipType = RotateFlipType.RotateNoneFlipNone; switch (item.Rotation) { case -1: rotateFlipType = RotateFlipType.Rotate270FlipNone; break; case 1: rotateFlipType = RotateFlipType.Rotate90FlipNone; break; default: break; } ; if (rotateFlipType != RotateFlipType.RotateNoneFlipNone) { image.RotateFlip(rotateFlipType); } // We re-encode the image to decrease the size. var codec = ImageCodecInfo.GetImageEncoders().FirstOrDefault(c => c.FormatID == ImageFormat.Jpeg.Guid); var encoderParams = new EncoderParameters(1); // Highest quality is 100. Quality affects the file size. Do not change it until you have exprimented. int quality = 50; // Do not pass inline. This parameter is passed via pointer and it has to be strongly typed. encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, quality); image.Save(memoryStream, codec, encoderParams); //image.Save(memoryStream, ImageFormat.Jpeg); } memoryStream.Seek(0, SeekOrigin.Begin); blobName = String.Format("{0}/{1}/{2}.jpg", userKey, timeKey, page); await AzureStorageUtils.UploadBlobAsync(memoryStream, AzureStorageUtils.ContainerNames.Artifacts, blobName, MediaType.Jpeg); blobNames.Add(blobName); } page++; } } var artifact = String.Join(",", blobNames); var exerciseId = await ExerciseUtils.CreateExercise(artifact, userId, serviceType, ArtifactType.Jpeg, 0, title, cardId, comment); return(RedirectToAction("View", "Exercises", new { Id = exerciseId })); }
// TODO OBSOLETE. Do not rename the files parameter. The form inputs are bounded by this name. public async Task <ActionResult> SaveRecordings(IEnumerable <HttpPostedFileBase> files, string serviceType, Guid?cardId, string title, string comment) { /* * 1. Validate input. * 2. Save the original media files to blobs. * 3. Call the remote transcoding service. The service converts files to MP3, merges them into a single file, saves it to a blob and returns its name and the recording's duration. * 4. Create a database record. * 5. Redirect to the exercise page. */ // 1. Validate input. // Find out the action to redirect to on error. We use the referrer string. RedirectToAction seems accept a case-insensitive parameter. var referrerAction = (Request.UrlReferrer.Segments.Skip(2).Take(1).SingleOrDefault() ?? "Index").Trim('/'); //referrerAction = referrerAction.First().ToString().ToUpper() + referrerAction.Substring(1).ToLower(); // var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext)); if (files == null || files.All(i => i == null)) { return(RedirectToAction(referrerAction, new { error = "no_file" })); } // There may be empty form parts from input elements with no file selected. var originalFiles = files.Where(i => i != null); var contentType = originalFiles .Select(i => i.ContentType) .Distinct() .Single() ; var acceptedContentTypes = (new[] { MediaType.Mpeg, MediaType.Mp3, MediaType.Amr, MediaType.Gpp, MediaType.QuickTime }); if (!acceptedContentTypes.Contains(contentType)) { return(RedirectToAction(referrerAction, new { error = "wrong_file_format" })); } // 2. Save the original media files to blobs. var userId = this.GetUserId(); var userIdKey = KeyUtils.IntToKey(userId); var timeKey = KeyUtils.GetTimeAsBase32(); var index = 0; var extension = MediaType.GetExtension(contentType); //var blobNames = new List<string>(); foreach (var file in originalFiles) { using (var inputStream = file.InputStream) { // The directory structure in the Blob Storage is userIdKey/timeKey/index.ext. Runnymede.Helper.Controllers.RecordingsController.Get() relies on this structure. var blobName = String.Format("{0}/{1}/{2}.{3}", userIdKey, timeKey, index, extension); await AzureStorageUtils.UploadBlobAsync(inputStream, AzureStorageUtils.ContainerNames.Artifacts, blobName, file.ContentType); index++; } } // 3. Call the remote transcoding service. var host = ConfigurationManager.AppSettings["RecorderHost"]; var urlFormat = "http://{0}/api/recordings/transcoded/?userIdKey={1}&timeKey={2}&extension={3}&count={4}"; var url = String.Format(urlFormat, host, userIdKey, timeKey, extension, originalFiles.Count()); HttpClient client = new HttpClient(); HttpResponseMessage response = await client.GetAsync(url); if (!response.IsSuccessStatusCode) { return(RedirectToAction(referrerAction, new { error = "transcoding_error" })); } // Error is returned as HTML. Then we get error here: No MediaTypeFormatter is available to read an object of type 'JObject' from content with media type 'text/html'. var recordingDetails = await response.Content.ReadAsAsync <RecordingDetails>(); // Make sure the duration is known. If the transcoder has failed to parse the ffmpeg logs, it returns DurationMsec = 0. if (recordingDetails.TotalDuration == 0) { // Read the blob and try to determine the duration directly. recordingDetails.TotalDuration = await RecordingUtils.GetMp3Duration(AzureStorageUtils.ContainerNames.Artifacts, recordingDetails.BlobName); } // 4. Create a database record. var exerciseId = await ExerciseUtils.CreateExercise(recordingDetails.BlobName, userId, serviceType, ArtifactType.Mp3, recordingDetails.TotalDuration, title, cardId, comment); // 5. Redirect to the exercise page. return(RedirectToAction("View", "Exercises", new { Id = exerciseId })); }