Monday, 30 March, 2015 UTC


Summary

Yves Bamani requested a post to build a media player app using Ionic Framework and Cordova. The mediaPlayer app will show a player interface and a couple of buttons. The user can browse through the filesystem and pick a file to play. The initial request was to play both Audio and Video files inline. But I was only able to get the Audio files to play inline where as the Video will be launched in the device default video player.
Here is a quick demo of the app
Yves also requested for the app to be built using ngCordova. But there are a couple of features in $cordovaMedia like
getDuration(media)
  and 
getCurrentPosition()
, which are not working yet. And the $cordovaFile does not have a method to recursively get all the folders in a directory, so I am going to use Cordova API as is and build this app.
You can find the completed here.
So, let us get started.
Prerequisites
You need to have the knowledge of the following to get a better understanding of the code below.
  • Angularjs
  • Ionic Framework
  • Cordova
Application Design
The application is pretty simple. When the user launches the app, we show a file browser, where the user can browse through his/her device. Then they can select an audio or video file to play.
We will be using a side menu template, where the player is a instance of 
$ionicModal
. This modal lives in the background. As mentioned earlier, we will be using the Media API from cordova to manage the audio files and a Video player Cordova plugin by Dawson Loudon named VideoPlayer inspired by Simon MacDonald’s VideoPlayer. All this plugin does is create a new Video Intent and launch it.
I am still looking for a consistent solution on how to implement inline videos. If you do know one, please drop a comment.
I have tested this app on an Android and not on iOS.
Develop the App
Create a new folder named mediaPlayerApp. Open a new terminal/prompt here. We will scaffold the side menu template and then clean it up as per our needs. Run,
ionic start mediaPlayerApp sidemenu
Once the scaffolding is completed, you can CD into the mediaPlayerApp folder and run
ionic serve
and you should see the side menu app in your browser.
The first thing we are going to do is update index.html. Open www/index.html in your favorite editor and update it as below
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    <title></title>
    <link href="lib/ionic/css/ionic.css" rel="stylesheet">
    <link href="css/style.css" rel="stylesheet">
    <!-- IF using Sass (run gulp sass first), then uncomment below and remove the CSS includes above
    <link href="css/ionic.app.css" rel="stylesheet">
    -->
    <!-- ionic/angularjs js -->
    <script src="lib/ionic/js/ionic.bundle.js"></script>
    <!-- cordova script (this will be a 404 during development) -->
    <script src="cordova.js"></script>
    <!-- your app's js -->
    <script src="js/app.js"></script>
    <script src="js/audio.service.js"></script>
    <script src="js/controllers.js"></script>
</head>

<body ng-app="starter">
    <ion-nav-view></ion-nav-view>
</body>

</html>
We have referred the audio service JS code that consist of the logic to deal with the Media API and the controllers file which consist of the controller logic.
Next, open www/templates/menu.html. We will update this file with fewer menu items
<ion-side-menus enable-menu-with-back-views="false">
    <ion-side-menu-content>
        <ion-nav-bar class="bar-assertive">
            <ion-nav-back-button>
            </ion-nav-back-button>
            <ion-nav-buttons side="left">
                <button class="button button-icon button-clear ion-navicon" menu-toggle="left">
                </button>
            </ion-nav-buttons>
        </ion-nav-bar>
        <ion-nav-view name="menuContent"></ion-nav-view>
    </ion-side-menu-content>
    <ion-side-menu side="left">
        <ion-header-bar class="bar-assertive">
            <h1 class="title">Menu</h1>
        </ion-header-bar>
        <ion-content>
            <ion-list>
                <ion-item nav-clear menu-close ng-click="player()">
                    Player
                </ion-item>
                <ion-item nav-clear menu-close href="#/app/browse">
                    Browse
                </ion-item>
            </ion-list>
        </ion-content>
    </ion-side-menu>
</ion-side-menus>
We have removed all the old menu items and added 2 new. One to show the player, and one to browse the device.
Now, we will clean up and rename the required files inside www/templates folder
  • Rename login.html to player.html
  • Delete playlist.html
  • Delete playlists.html
  • Delete search.html
Open www/templates/player.html and update it as below
<ion-modal-view>
    <ion-header-bar class="bar-assertive">
        <h1 class="title">Player</h1>
        <div class="buttons">
            <button class="button button-clear" ng-click="hidePlayer()">Hide</button>
        </div>
    </ion-header-bar>
    <ion-content class="padding">
        <div class="list card">
            <div class="item">
                <h2><i class="icon ion-ios-musical-notes"></i> {{name || '--'}}</h2>
                <p>{{path || '--'}}</p>
            </div>
            <div class="item item-body">
                <div ng-hide="loaded">
                    <label>Please select a Media file to play</label>
                </div>
                <div ng-show="loaded" style="height: 30px; width: {{position}}%; transition: width 0.2s; background: #e42012;">
                </div>
            </div>
            <div class="item tabs tabs-secondary tabs-icon-left">
                <a class="tab-item" href="javascript:" ng-click="resumeAudio()" ng-hide="isPlaying || !loaded">
                    <i class="icon ion-ios-play"></i> Play
                </a>
                <a class="tab-item" href="javascript:" ng-click="pauseAudio()" ng-show="isPlaying">
                    <i class="icon ion-pause"></i> Pause
                </a>
                <a class="tab-item" href="javascript:" ng-click="stopAudio()" ng-show="isPlaying">
                    <i class="icon ion-stop"></i> Stop
                </a>
            </div>
        </div>
    </ion-content>
</ion-modal-view>
This template will be shown when we init the Ionic Modal. This Modal will always be in the background while the app is running.
This template is the base for showing the audio player. Line 18 consists of the logic to show the seek bar. Line 22 shows the play button, when we have paused the video using the button on line 25. We can also stop the media play back completely using the Stop button.
As you can see from the directives on these buttons, they are subjected to visibility based on the state of the audio file.
Next, open www/templates/browse.html and update it as below
<ion-view view-title="Browse">
    <ion-content>
        <ion-item class="item-icon-left item-icon-right" ng-repeat="file in files" type="item-text-wrap" ng-click="showSubDirs(file)">
            <i class="icon" ng-class="{'ion-android-folder' : file.isDirectory, 'ion-eye' : (!file.isDirectory && !file.isUpNav), 'ion-arrow-up-c' : file.isUpNav}"></i>
            <h2>{{file.name}}</h2>
            <p>Location : {{file.fullPath}}</p>
            <i class="icon ion-chevron-right icon-accessory" ng-show="file.isDirectory"></i>
        </ion-item>
    </ion-content>
</ion-view>
This is a very simple template drive by the
files[]
  from scope. When the user launches the browse page, we will query the file system and show the root files. Once the user clicks on a particular folder, we query the file system API passing in the current folder as the root and fetch its children.
And when a user selects a file, we check if it is of the type audio or video and then play it. Else we will show a message that we cannot play the selected file.
This completes all our templates. We will now work with the Audio Service. Open www/js folder and create a new file named audio.service.js and update it as below.
angular.module('starter.services', [])

.service('AudioSvc', [function() {

  var AudioSvc = {
    my_media: null,
    mediaTimer: null,
    playAudio: function(src, cb) {
      var self = this;

      // stop playing, if playing
      self.stopAudio();

      self.my_media = new Media(src, onSuccess, onError);
      self.my_media.play();

      if (self.mediaTimer == null) {
        self.mediaTimer = setInterval(function() {
          self.my_media.getCurrentPosition(
            function(position) {
              cb(position, self.my_media.getDuration());
            },
            function(e) {
              console.log("Error getting pos=" + e);
            }
          );
        }, 1000);
      }

      function onSuccess() {
        console.log("playAudio():Audio Success");
      }

      // onError Callback
      //
      function onError(error) {
        // alert('code: ' + error.code + '\n' +
        //     'message: ' + error.message + '\n');

        // we forcefully stop

      }

    },

    resumeAudio: function() {
      var self = this;
      if (self.my_media) {
        self.my_media.play();
      }
    },
    pauseAudio: function() {
      var self = this;
      if (self.my_media) {
        self.my_media.pause();
      }
    },
    stopAudio: function() {
      var self = this;
      if (self.my_media) {
        self.my_media.stop();
      }
      if (self.mediaTimer) {
        clearInterval(self.mediaTimer);
        self.mediaTimer = null;
      }
    }

  };

  return AudioSvc;
}])
Things to notice
Line 1 : We have created a new module named
starter.services
 and added the 
AudioSvc
 to that.
Line 8 : Defines the play audio method, which plays the given src using the Cordova’s Media API.
Line 18 : We create a
setInterval
 for every seconds to execute 
getCurrentPosition()
 and trigger the callback. This way we update the seek bar with the total time and time left.
Line 46 : The logic for resume audio
Line 52 : The logic for pause audio
Line 58 : The logic for stop audio
We will be injecting this service as a dependency in our controller, where we will manage the above API based on the user interactions.
Next, we will update www/js/app.js as below
angular.module('starter', ['ionic', 'starter.controllers', 'starter.services'])

.run(function($ionicPlatform, $rootScope, $ionicLoading) {
  $ionicPlatform.ready(function() {
    if (window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if (window.StatusBar) {
      StatusBar.styleDefault();
    }

    $rootScope.toggle = function(text, timeout) {
      $rootScope.show(text);

      setTimeout(function() {
        $rootScope.hide();
      }, (timeout || 1000));
    };

    $rootScope.show = function(text) {
      $ionicLoading.show({
        template: text
      });
    };

    $rootScope.hide = function() {
      $ionicLoading.hide();
    };
  });
})

.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider

    .state('app', {
    url: "/app",
    abstract: true,
    templateUrl: "templates/menu.html",
    controller: 'AppCtrl'
  })


  .state('app.browse', {
    url: "/browse",
    views: {
      'menuContent': {
        templateUrl: "templates/browse.html",
        controller: 'BrowseCtrl'
      }
    }
  })

  $urlRouterProvider.otherwise('/app/browse');
});
Things to notice
Line 1 : We create a module named starter and inject ionicstarter.controllers and starter.services as dependencies.
Line 4 : Inside the 
$ionicPlatform.ready()
 we config the keyboard and status bar.
Line 12, 20, 26 : We are building an API to work with the 
$ionicLoading
  API.
Line 32 : We config the router
Finally, the controllers. Open www/js/controller/js and update it as below
angular.module('starter.controllers', [])

.controller('AppCtrl', ['$rootScope', '$scope', function($rootScope, $scope) {
  $scope.player = function() {
    $rootScope.player();
  }
}])

.controller('BrowseCtrl', ['$window', '$ionicPlatform', '$rootScope', '$scope', '$ionicScrollDelegate', 'AudioSvc', '$ionicModal',
  function($window, $ionicPlatform, $rootScope, $scope, $ionicScrollDelegate, AudioSvc, $ionicModal) {
    $scope.files = [];

    $ionicModal.fromTemplateUrl('templates/player.html', {
      scope: $scope
    }).then(function(modal) {
      $scope.modal = modal;
    });

    $rootScope.hidePlayer = function() {
      $scope.modal.hide();
    };

    $rootScope.player = function() {
      $scope.modal.show();
    };

    $ionicPlatform.ready(function() {

      $rootScope.show('Accessing Filesystem.. Please wait');
      $window.requestFileSystem($window.LocalFileSystem.PERSISTENT, 0, function(fs) {
          //console.log("fs", fs);

          var directoryReader = fs.root.createReader();

          directoryReader.readEntries(function(entries) {
              var arr = [];
              processEntries(entries, arr); // arr is pass by refrence
              $scope.files = arr;
              $rootScope.hide();
            },
            function(error) {
              console.log(error);
            });
        },
        function(error) {
          console.log(error);
        });

      $scope.showSubDirs = function(file) {

        if (file.isDirectory || file.isUpNav) {
          if (file.isUpNav) {
            processFile(file.nativeURL.replace(file.actualName + '/', ''));
          } else {
            processFile(file.nativeURL);
          }
        } else {
          if (hasExtension(file.name)) {
            if (file.name.indexOf('.mp4') > 0) {
              // Stop the audio player before starting the video
              $scope.stopAudio();
              VideoPlayer.play(file.nativeURL);
            } else {
              fsResolver(file.nativeURL, function(fs) {
                //console.log('fs ', fs);
                // Play the selected file
                AudioSvc.playAudio(file.nativeURL, function(a, b) {
                  //console.log(a, b);
                  $scope.position = Math.ceil(a / b * 100);
                  if (a < 0) {
                    $scope.stopAudio();
                  }
                  if (!$scope.$$phase) $scope.$apply();
                });

                $scope.loaded = true;
                $scope.isPlaying = true;
                $scope.name = file.name;
                $scope.path = file.fullPath;

                // show the player
                $scope.player();

                $scope.pauseAudio = function() {
                  AudioSvc.pauseAudio();
                  $scope.isPlaying = false;
                  if (!$scope.$$phase) $scope.$apply();
                };
                $scope.resumeAudio = function() {
                  AudioSvc.resumeAudio();
                  $scope.isPlaying = true;
                  if (!$scope.$$phase) $scope.$apply();
                };
                $scope.stopAudio = function() {
                  AudioSvc.stopAudio();
                  $scope.loaded = false;
                  $scope.isPlaying = false;
                  if (!$scope.$$phase) $scope.$apply();
                };

              });
            }
          } else {
            $rootScope.toggle('Oops! We cannot play this file :/', 3000);
          }

        }

      }

      function fsResolver(url, callback) {
        $window.resolveLocalFileSystemURL(url, callback);
      }

      function processFile(url) {
        fsResolver(url, function(fs) {
          //console.log(fs);
          var directoryReader = fs.createReader();

          directoryReader.readEntries(function(entries) {
              if (entries.length > 0) {
                var arr = [];
                // push the path to go one level up
                if (fs.fullPath !== '/') {
                  arr.push({
                    id: 0,
                    name: '.. One level up',
                    actualName: fs.name,
                    isDirectory: false,
                    isUpNav: true,
                    nativeURL: fs.nativeURL,
                    fullPath: fs.fullPath
                  });
                }
                processEntries(entries, arr);
                $scope.$apply(function() {
                  $scope.files = arr;
                });

                $ionicScrollDelegate.scrollTop();
              } else {
                $rootScope.toggle(fs.name + ' folder is empty!', 2000);
              }
            },
            function(error) {
              console.log(error);
            });
        });
      }

      function hasExtension(fileName) {
        var exts = ['.mp3', '.m4a', '.ogg', '.mp4', '.aac'];
        return (new RegExp('(' + exts.join('|').replace(/\./g, '\\.') + ')$')).test(fileName);
      }

      function processEntries(entries, arr) {

        for (var i = 0; i < entries.length; i++) {
          var e = entries[i];

          // do not push/show hidden files or folders
          if (e.name.indexOf('.') !== 0) {
            arr.push({
              id: i + 1,
              name: e.name,
              isUpNav: false,
              isDirectory: e.isDirectory,
              nativeURL: e.nativeURL,
              fullPath: e.fullPath
            });
          }
        }
        return arr;
      }

    });
  }
])
Things to notice
Line 13 : When the app launches, the BrowseCtrl is invoked. Here, we initialize the player as a modal from the template.
Line 19, 23 : We have created 2 methods on 
$rootScope
 that can show and hide the player. This will be used across the app. Take a look at line 5.
Line 27 : The File System traversing starts from here. We wait for the device to be ready.
Line 30 : We call 
requestFileSystem()
 on the window and get the contents of the root directory.
Line 35 : We read all the entries present in the root folder and  call 
processEntries()
 to build an Array of files system items.
Line 156 : Here we get all the entries and a reference Array. We iterate through each item and build an object, which consists of essential information while rendering. This same method will be invoked when ever we are reading entries from the file system and building a UI.
Line 38 : We assign the file info array to 
$scope.files
. This updates the browse.html template to reflect the files in the root.
Line 49 : Will get invoked when any file or folder name is clicked.
Line 51 : If the selected item is a folder, we check if the item is a navigation item. Navigation items are used to move to folder one level up, which we create for the user to navigate. Based on this condition, we call 
processFile()
 with a URL
Line 115 : Here, we resolve the current URL, and then get the children (files/folders) inside that path and then update 
$scope.files
.
Line 125 : If the folder is not the root folder and it has children, we append a new 
.. One level up
 item to the top of the list, using which the user can navigate to the parent folder. You can see the same in the demo video. This way, we can recursively show the file system to the user.
Line 57 : If the clicked item is a file, we check it is an Audio file or Video file. If it is a Video file, we will invoke the 
VideoPlayer.play()
 passing in the media URL. We install the Video player plugin next.
Line 63 : If it is an audio file, we will work with the 
AudioSvc
  service and manage the player.
The file looks pretty complicated but the logic is pretty simple.
Install Cordova Plugins
To run the app, we need 3 plugins.
  1. com.ionic.keyboard
  2. org.apache.cordova.file
  3. org.apache.cordova.media
  4. com.dawsonloudon.videoplayer
To install the plugins run
cordova plugin add com.ionic.keyboard
cordova plugin add org.apache.cordova.file
cordova plugin add org.apache.cordova.media
cordova plugin add https://github.com/dawsonloudon/VideoPlayer.git
Run the App
We need to run the app on a device to test it. First, we will add a platform. Run,
ionic platform add android
And then to the run  the app on a device execute
ionic run
And you should be able to check out the below
 
Hope this post gave you an idea on how to work with File System, Audio and Video APIs.

Thanks for reading! Do comment.
@arvindr21
The post Ionic Framework, Cordova and File API – A Media Player App appeared first on The Jackal of Javascript.