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 <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 })); } }
// 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 })); }