Пример #1
0
        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));
        }
Пример #2
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 }));
            }
        }
Пример #3
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 <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 }));
        }