diff --git a/Homarus/README.md b/Homarus/README.md new file mode 100644 index 00000000..07ee4340 --- /dev/null +++ b/Homarus/README.md @@ -0,0 +1,58 @@ +# Homarus + +## Introduction + +[FFmpeg](https://www.ffmpeg.org/) as a microservice. + +## Installation +- Install `ffmpeg`. On Ubuntu, this can be done with `sudo apt-get install ffmpeg`. +- Clone this repository somewhere in your web root (example: `/var/www/html/Crayfish/Homarus`). +- Copy `/var/www/html/Crayfish/Homarus/cfg/config.default.yml` to `/var/www/html/Crayfish/Homarus/cfg/config.yml` +- Copy `/var/www/html/Crayfish/Hypercube/syn-settings.xml` to `/var/www/html/Crayfish/Homarus/syn-settings.xml` +- Install `composer`. [Install instructions here.][4] +- `$ cd /path/to/Homarus` and run `$ composer install` +- Then either + - For production, configure your web server appropriately (e.g. add a VirtualHost for Homarus in Apache) OR + - For development, run the PHP built-in webserver `$ php -S localhost:8888 -t src` from Homarus root. + + +### Apache2 + +To use Homarus with Apache you need to configure your Virtualhost with a few options: +- Redirect all requests to the Homarus index.php file +- Make sure Hypercube has access to Authorization headers + +Here is an example configuration for Apache 2.4: +```apache +Alias "/homarus" "/var/www/html/Crayfish/Homarus/src" + + FallbackResource /homarus/index.php + Require all granted + DirectoryIndex index.php + SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 + +``` + +This will put the Homarus at the /homarus endpoint on the webserver. + +## Configuration + +If your ffmpeg installation is not on your path, then you can configure homarus to use a specific executable by editing `executable` entry in [config.yaml](./cfg/config.example.yaml). + +You also will need to set the `fedora base url` entry to point to your Fedora installation. + +## Usage +This will return the an avi file for the test video file in Fedora. +``` +curl -H "Authorization: Bearer islandora" -H "Accept: video/x-msvideo" -H "Apix-Ldp-Resource:http://localhost:8080/fcrepo/rest/testvideo" http://localhost:8000/homarus/convert --output output.avi +``` + +## Maintainers + +Current maintainers: + +* [Natkeeran](https://github.com/Natkeeran) + +## License + +[MIT](https://opensource.org/licenses/MIT) diff --git a/Homarus/cfg/config.example.yaml b/Homarus/cfg/config.example.yaml new file mode 100644 index 00000000..f3b36314 --- /dev/null +++ b/Homarus/cfg/config.example.yaml @@ -0,0 +1,37 @@ +--- +homarus: + # path to the ffmpeg executable + executable: ffmpeg + mime_types: + valid: + - video/mp4 + - video/x-msvideo + - video/ogg + default_video: video/mp4 + mime_to_format: + valid: + - video/mp4_mp4 + - video/x-msvideo_avi + - video/ogg_ogg + +fedora_resource: + base_url: http://localhost:8080/fcrepo/rest + +log: + # Valid log levels are: + # DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY, NONE + # log level none won't open logfile + level: DEBUG + file: /var/log/islandora/homarus.log + +syn: + # toggles JWT security for service + enable: True + # Path to the syn config file for authentication. + # example can be found here: + # https://github.com/Islandora-CLAW/Syn/blob/master/conf/syn-settings.example$ + config: ../syn-settings.xml + + + + diff --git a/Homarus/composer.json b/Homarus/composer.json new file mode 100644 index 00000000..eaac1de2 --- /dev/null +++ b/Homarus/composer.json @@ -0,0 +1,43 @@ +{ + "name": "islandora/homarus", + "description": "FFmpeg as a web service", + "type": "project", + "require": { + "islandora/crayfish-commons": "dev-master" + }, + "license": "MIT", + "authors": [ + { + "name": "Islandora Foundation", + "email": "community@islandora.ca", + "role": "Owner" + }, + { + "name": "Natkeeran L.Kanthan", + "email": "nat.ledchumykanthan@utoronto.ca", + "role": "Maintainer" + } + ], + "autoload": { + "psr-4": { + "Islandora\\Homarus\\": "src/" + } + }, + "scripts": { + "check": [ + "phpcs --standard=PSR2 src tests", + "phpcpd --names *.php src" + ], + "test": [ + "@check", + "phpunit" + ] + }, + "require-dev": { + "symfony/browser-kit": "^3.0", + "symfony/css-selector": "^3.0", + "phpunit/phpunit": "^5.0", + "squizlabs/php_codesniffer": "^2.0", + "sebastian/phpcpd": "^3.0" + } +} diff --git a/Homarus/phpunit.xml.dist b/Homarus/phpunit.xml.dist new file mode 100644 index 00000000..c14c1607 --- /dev/null +++ b/Homarus/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + ./tests/ + + + + + + + + + ./src/index.php + ./src/app.php + + ./src + + + diff --git a/Homarus/src/Controller/HomarusController.php b/Homarus/src/Controller/HomarusController.php new file mode 100644 index 00000000..bda261b4 --- /dev/null +++ b/Homarus/src/Controller/HomarusController.php @@ -0,0 +1,144 @@ +cmd = $cmd; + $this->formats = $formats; + $this->default_format = $default_format; + $this->executable = $executable; + $this->log = $log; + $this->mime_to_format = $mime_to_format; + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * @return \Symfony\Component\HttpFoundation\Response|\Symfony\Component\HttpFoundation\StreamedResponse + */ + public function convert(Request $request) { + $this->log->info('Ffmpeg Convert request.'); + + // Short circuit if there's no Apix-Ldp-Resource header. + if (!$request->headers->has("Apix-Ldp-Resource")) + { + $this->log->debug("Malformed request, no Apix-Ldp-Resource header present"); + return new Response( + "Malformed request, no Apix-Ldp-Resource header present", + 400 + ); + } else { + $source = $request->headers->get('Apix-Ldp-Resource'); + } + + // Find the format + $content_types = $request->getAcceptableContentTypes(); + $content_type = $this->get_content_type($content_types); + $format = $this->get_ffmpeg_format($content_type); + + $cmd_params = ""; + if($format == "mp4") { + $cmd_params = " -vcodec libx264 -preset medium -acodec aac -strict -2 -ab 128k -ac 2 -async 1 -movflags frag_keyframe+empty_moov "; + } + + // Arguments to ffmpeg command are sent as a custom header + $args = $request->headers->get('X-Islandora-Args'); + $this->log->debug("X-Islandora-Args:", ['args' => $args]); + + $cmd_string = "$this->executable -i $source $cmd_params -f $format -"; + $this->log->info('Ffempg Command:', ['cmd' => $cmd_string]); + + // Return response. + try { + return new StreamedResponse( + $this->cmd->execute($cmd_string, $source), + 200, + ['Content-Type' => $content_type] + ); + } catch (\RuntimeException $e) { + $this->log->error("RuntimeException:", ['exception' => $e]); + return new Response($e->getMessage(), 500); + } + } + + + private function get_content_type($content_types) { + $content_type = null; + foreach ($content_types as $type) { + if (in_array($type, $this->formats)) { + $content_type = $type; + break; + } + } + + if ($content_type === null) { + $content_type = $this->default_format; + $this->log->info('Falling back to default content type'); + } + return $content_type; + } + + private function get_ffmpeg_format($content_type){ + foreach ($this->mime_to_format as $format) { + if (strpos($format, $content_type) !== false) { + $this->log->info("does it get here"); + $format_info = explode("_", $format); + break; + } + } + return $format_info[1]; + } + + /** + * @return \Symfony\Component\HttpFoundation\BinaryFileResponse + */ + public function convertOptions() + { + return new BinaryFileResponse( + __DIR__ . "/../../static/convert.ttl", + 200, + ['Content-Type' => 'text/turtle'] + ); + } + +} diff --git a/Homarus/src/app.php b/Homarus/src/app.php new file mode 100644 index 00000000..dca69e57 --- /dev/null +++ b/Homarus/src/app.php @@ -0,0 +1,30 @@ +register(new IslandoraServiceProvider()); +$app->register(new YamlConfigServiceProvider(__DIR__ . '/../cfg/config.yaml')); + +$app['homarus.controller'] = function ($app) { + return new HomarusController( + $app['crayfish.cmd_execute_service'], + $app['crayfish.homarus.mime_types.valid'], + $app['crayfish.homarus.mime_types.default_video'], + $app['crayfish.homarus.executable'], + $app['monolog'], + $app['crayfish.homarus.mime_to_format'] + ); +}; + +$app->options('/convert', "homarus.controller:convertOptions"); +$app->get('/convert', "homarus.controller:convert"); + +return $app; diff --git a/Homarus/src/index.php b/Homarus/src/index.php new file mode 100644 index 00000000..3e23d477 --- /dev/null +++ b/Homarus/src/index.php @@ -0,0 +1,4 @@ +run(); diff --git a/Homarus/static/convert.ttl b/Homarus/static/convert.ttl new file mode 100644 index 00000000..ccda85c7 --- /dev/null +++ b/Homarus/static/convert.ttl @@ -0,0 +1,18 @@ +@prefix apix: . +@prefix owl: . +@prefix ebucore: . +@prefix ldp: . +@prefix islandora: . +@prefix rdfs: . + +<> a apix:Extension; + rdfs:label "Ffmpeg Service"; + rdfs:comment "ffmpeg as a microservice"; + apix:exposesService islandora:ConvertService; + apix:exposesServiceAt "svc:convert"; + apix:bindsTo <#class> . + +<#class> owl:intersectionOf ( + ldp:NonRDFSource + [ a owl:Restriction; owl:onProperty ebucore:hasMimeType; owl:hasValue "video/mp4", "video/x-msvideo", "video/ogg" ] +) . \ No newline at end of file diff --git a/Homarus/tests/Islandora/Homarus/Tests/HomarusControllerTest.php b/Homarus/tests/Islandora/Homarus/Tests/HomarusControllerTest.php new file mode 100644 index 00000000..0a3c319f --- /dev/null +++ b/Homarus/tests/Islandora/Homarus/Tests/HomarusControllerTest.php @@ -0,0 +1,44 @@ +prophesize(CmdExecuteService::class)->reveal(); + $controller = new HomarusController( + $mock_service, + [], + '', + 'convert', + $this->prophesize(Logger::class)->reveal(), + '' + ); + + $response = $controller->convertOptions(); + $this->assertTrue($response->getStatusCode() == 200, 'Convert OPTIONS should return 200'); + $this->assertTrue( + $response->headers->get('Content-Type') == 'text/turtle', + 'Convert OPTIONS should return turtle' + ); + } + +}