コード例 #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 <ActionResult> Upload(HttpPostedFileBase mp3File, HttpPostedFileBase xmlFile,
                                                string userdir = "", string title = null, string learnerSkype = null)
        {
            int exerciseId = 0;
            int newUserId  = 0;

            var userId = this.GetUserId();

            // Save the recording
            if (mp3File != null)
            {
                ViewBag.mp3FileName = mp3File.FileName;
                // We can receive garbage from the wild web.
                try
                {
                    using (var stream = mp3File.InputStream)
                    {
                        var duration = RecordingUtils.GetMp3Duration(stream);
                        if (duration > 0)
                        {
                            var recordingTitle = !String.IsNullOrEmpty(title)
                                ? title
                                : Path.GetFileNameWithoutExtension(mp3File.FileName);

                            exerciseId = await ExerciseUtils.SaveRecording(
                                stream,
                                userId,
                                ArtifactType.Mp3,
                                ServiceType.IeltsSpeaking,
                                duration,
                                recordingTitle
                                );
                        }
                    }
                }
                catch
                {
                }
            }

            // 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())
            {
                // 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.
                if (!String.IsNullOrEmpty(learnerSkype))
                {
                    newUserId = (await DapperHelper.QueryResilientlyAsync <int?>("dbo.exeTryChangeExerciseAuthor",
                                                                                 new
                    {
                        ExerciseId = exerciseId,
                        UserId = userId,
                        SkypeName = learnerSkype,
                    },
                                                                                 CommandType.StoredProcedure))
                                .SingleOrDefault()
                                .GetValueOrDefault();
                }

                IEnumerable <int> remarkSpots = new int[] { };

                // If there is a remark spot collection XML file produced by the teacher coming with the recording.
                if (xmlFile != null)
                {
                    // We can receive garbage from the wild web.
                    try
                    {
                        XElement root;
                        using (var stream = xmlFile.InputStream)
                        {
                            root = XElement.Load(stream);
                        }

                        var startedText = root.Attribute("time").Value;
                        var started     = DateTime.Parse(startedText, null, DateTimeStyles.RoundtripKind);

                        remarkSpots = root
                                      .Elements("RemarkSpot")
                                      .Select(i =>
                        {
                            var spot = DateTime.Parse(i.Attribute("time").Value, null, DateTimeStyles.RoundtripKind);
                            return(Convert.ToInt32((spot - started).TotalMilliseconds));
                        })
                                      .ToList();
                    }
                    catch
                    {
                    }
                }

                // We have got remark spots. Create a review.
                if (remarkSpots.Any())
                {
                    var reviewId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeCreateUploadedReview", new
                    {
                        ExerciseId = exerciseId,
                        UserId = userId,
                    },
                                                                                   CommandType.StoredProcedure
                                                                                   ))
                                   .SingleOrDefault();

                    // Save remarks.
                    if (reviewId != 0)
                    {
                        var pieces = remarkSpots
                                     .OrderBy(i => i)
                                     .Distinct()
                                     .Select((i, index) =>
                        {
                            var finish = Math.Max(0, i - 1000);      // We allow for a 1 second delay between the actual learner's mistake and the teacher's action.
                            var start  = Math.Max(0, finish - 2000); // Assume the spot is 2 second long.

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

                            return(new ReviewPiece
                            {
                                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);

                        // Redirect to the reviews page.
                        return(RedirectToAction("Edit", "Reviews", new { id = reviewId.ToString() }));
                    }
                } // end of if (remarkSpots.Any())
            }     // end of if (exerciseId && this.GetIsTeacher())

            //
            if (exerciseId != 0)
            {
                return(newUserId == 0 ? RedirectToAction("Index") : RedirectToAction("Index", "Reviews"));
            }

            ViewBag.Success = exerciseId != 0;
            ViewBag.UserDir = userdir;

            return(View());
        } // end of Upload()
コード例 #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 }));
        }
コード例 #4
0
        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));
        }
コード例 #5
0
        // 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 }));
        }