예제 #1
0
        public async Task <string> SaveAudiorRecording(string recorderId, string recordName, double duration)
        {
            bool success = false;

            try
            {
                var userId = this.GetUserId();
                // recorderId comes from Audior, 13 digits, milliseconds, current Date in the browser. It is part of Artifact and will be used to update Title with the real title after upload is done.
                var blobName = UploadUtils.ConstractArtifactBlobName(userId, recorderId, recordName);

                using (var stream = Request.InputStream)
                {
                    await AzureStorageUtils.UploadBlobAsync(stream, AzureStorageUtils.ContainerNames.Artifacts, blobName, MediaType.Mp3);
                }

                var length = UploadUtils.DurationToLength(duration);
                await ExerciseUtils.CreateExercise(blobName, userId, ServiceType.IeltsSpeaking, ArtifactType.Mp3, length, null);

                success = true;
            }
            catch (Exception ex)
            {
                return("save=failed\n" + ex.Message);
            }
            // Audior expects only "save=..." in response.
            return("save=" + (success ? "ok" : "failed"));
        }
예제 #2
0
        public async Task <ActionResult> Claim(string id)
        {
            var userIdKey    = KeyUtils.IntToKey(0);
            var longTimeKey  = id + this.GetExtId();
            var blobName     = ExerciseUtils.FormatBlobName(userIdKey, longTimeKey, "metadata", "json");
            var blob         = AzureStorageUtils.GetBlob(AzureStorageUtils.ContainerNames.Artifacts, blobName);
            var metadataJson = await blob.DownloadTextAsync();

            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 details          = metadata["recordingDetails"];
            var recordingDetails = details.ToObject <RecordingDetails>();

            var exerciseId = await ExerciseUtils.CreateExercise(recordingDetails.BlobName, this.GetUserId(),
                                                                serviceType, ArtifactType.Mp3, recordingDetails.TotalDuration, title, cardId, comment, details.ToString(Formatting.None));

            //~~ Redirect to the View exercise page.
            return(RedirectToAction("View", new { Id = exerciseId }));
        }
예제 #3
0
        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 }));
            }
        }
예제 #4
0
        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> PutSave([FromUri] string uuid, [FromBody] JObject values)
        {
            string redirectToUrl = null;

            var learnerSkype = (string)values["learnerSkype"];
            var titleValue   = (string)values["title"];
            var comment      = (string)values["comment"];
            var remarkSpots  = ((JArray)values["remarkSpots"]).Select(i => (int)i); // milliseconds

            var userId = this.GetUserId();

            //-- Call the remote transcoding service.
            var                 host      = ConfigurationManager.AppSettings["RecorderHost"];
            var                 urlFormat = "http://{0}/api/recordings/from_freeswitch/?uuid={1}&userId={2}";
            var                 url       = String.Format(urlFormat, host, uuid, userId);
            HttpClient          client    = new HttpClient();
            HttpResponseMessage response  = await client.GetAsync(url);

            if (!response.IsSuccessStatusCode)
            {
                throw new HttpException("from_freeswitch");
            }
            // Error is returned as HTML. Then we get error here: No MediaTypeFormatter is available to read an object of type 'RecordingInfo' 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 title = !String.IsNullOrEmpty(titleValue)
                ? titleValue
                : Path.GetFileNameWithoutExtension(recordingDetails.BlobName);

            //-- Create a database record for the exercise.
            var exerciseId = await ExerciseUtils.CreateExercise(recordingDetails.BlobName, userId,
                                                                ServiceType.IeltsSpeaking, ArtifactType.Mp3, recordingDetails.TotalDuration, title, null, comment);

            // If the recording is being uploaded by a teacher, make it owned by the learner, create a review, and save remark spots if an accompanying XML file is provided.
            if (exerciseId != 0 && this.GetUserIsTeacher())
            {
                //-- Change the exercise author.
                // We may succeed or fail with updating the user of the exercise depending on whether the provided Skype name is found and is unambiguous.
                // Continue anyway with either old or new user.
                var newUserId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeTryChangeExerciseAuthor",
                                                                                new
                {
                    ExerciseId = exerciseId,
                    UserId = userId,
                    SkypeName = learnerSkype,
                },
                                                                                CommandType.StoredProcedure))
                                .SingleOrDefault();

                //-- Create a review. We do not charge the learner for a review initiated by the teacher.
                var reviewId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeCreateUploadedReview",
                                                                               new
                {
                    ExerciseId = exerciseId,
                    UserId = userId,
                },
                                                                               CommandType.StoredProcedure))
                               .SingleOrDefault();

                //-- We have got remark spots. Save remarks.
                if (reviewId != 0 && remarkSpots.Any())
                {
                    var pieces = remarkSpots
                                 .OrderBy(i => i)
                                 .Distinct()
                                 .Select((i, index) =>
                    {
                        var finish = Math.Max(0, i - AssumedTeacherReactionDelayMSec);
                        var start  = Math.Max(0, finish - DefaultSpotLengthMSec);

                        var remark = new RemarkSpot
                        {
                            ReviewId = reviewId,
                            Type     = ReviewPiece.PieceTypes.Remark,
                            Id       = index,
                            Start    = start,
                            Finish   = finish,
                        };

                        return(new ReviewPiece
                        {
                            // This PartitionKey calculation is redundunt, but the overhead is neglectable.
                            PartitionKey = ReviewPiece.GetPartitionKey(exerciseId),
                            RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Remark, index),
                            Json = JsonUtils.SerializeAsJson(remark),
                        });
                    });

                    var batchOperation = new TableBatchOperation();
                    foreach (var piece in pieces)
                    {
                        batchOperation.InsertOrReplace(piece);
                    }

                    //-- Write entities which will allow the reviewer to access remarks for reading and writing. We will simply check the presence of one of these records as we read or write the entities.
                    // The write entity will be deleted on review finish.
                    var viewerEntity = new ReviewPiece()
                    {
                        PartitionKey = ReviewPiece.GetPartitionKey(exerciseId),
                        RowKey       = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, userId),
                    };
                    batchOperation.InsertOrReplace(viewerEntity);

                    var editorEntity = new ReviewPiece()
                    {
                        PartitionKey = ReviewPiece.GetPartitionKey(exerciseId),
                        RowKey       = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId),
                    };
                    batchOperation.InsertOrReplace(editorEntity);

                    var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces);
                    await table.ExecuteBatchAsync(batchOperation);
                } // end of if (reviewId != 0 && remarkSpots.Any())

                //-- Return the link to the review page. The client will redirect.
                redirectToUrl = Url.Link("Default", new
                {
                    Controller = "Reviews",
                    Action     = reviewId != 0 ? "Edit" : "Index",
                    id         = reviewId.ToString()
                });
            } // end of if (exerciseId && this.GetIsTeacher())

            return(Created(redirectToUrl, redirectToUrl));
        }
        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 }));
        }