30

I have a list of audio files presented as links and an <audio> html5 player. Each link invokes a function which change the src of the <source> tag within the <audio>:

<audio controls="controls" preload="none">
  <source type="audio/mpeg" src="{{selectedSongPath}}"/>
</audio>

...

<div class="songEntry" ng-repeat="song in songs">
  <a href="" ng-click="songSelect(song.path)">{{song.name}}</a>
</div>

...

$scope.songSelect = function(songPath) {
    $scope.selectedSongPath = songPath;
}

I can see the src changing, but nothing is played. The paths are ok; if I initialize the src with one of the paths, the player works.

What am I doing wrong?

user1639431
  • 3,693
  • 8
  • 24
  • 22

12 Answers12

26

Here is a couple of angular approaches :

1) Use ng-src= instead of src=

The angular docs explain why this works : http://docs.angularjs.org/api/ng.directive:ngSrc

During compilation stage, angular will expand the element to include the correct src= attribute.

This will change the src attrib of the HTML5 audio element, but unfortunately that is NOT enough to make a new song play. You need to prod the audio element into doing something by calling the .play() method.

Sucks :(

Further hacking along this same path suggests doing DOM manipulation inside the controller. This is generally a sign that this is the wrong solution.

Better solution is to use services !!!

2) Audio using an angular service

// Define a simple audio service 
mpApp.factory('audio',function ($document) {
  var audioElement = $document[0].createElement('audio'); // <-- Magic trick here
  return {
    audioElement: audioElement,

    play: function(filename) {
        audioElement.src = filename;
        audioElement.play();     //  <-- Thats all you need
    }
    // Exersise for the reader - extend this service to include other functions
    // like pausing, etc, etc.

  }
});

Now, in your controller(s), you can inject 'audio', and do whatever you need to do with it.

eg:

function myAstoundingAudioCtrl($scope, audio) { //<-- NOTE injected audio service

    $scope.songSelect = function(songPath) {
        audio.play(songPath);    //     <---  Thats All You Need !
    }
}

You can now add 'audio' as a parameter to any of your controllers that need to be able to change the music, and use the same API call defined here.

Being a 'service', there is only a single instance for the whole application. All calls to 'audio' point to the same object. This is true for all services in angular. Just what we need in this case.

The service creates an invisible HTML5 element on the root document of your application, so there is no need to add an tag on your views anywhere. This has the added bonus of maintaining the playing of the song whilst the user navigates to different views.

See http://docs.angularjs.org/api/ng.$document for the definition of the $document service.

Hope that helps mate :)

SteveOC 64
  • 361
  • 3
  • 6
20

I had the same problem, even after using $sce.trustAsResourceUrl, and then I realized the problem is with the HTML. The source should go in the audio tag itself:

<audio controls data-ng-src="{{yourTrustedUrl}}" ></audio>
Oranit Dar
  • 1,539
  • 18
  • 17
  • 2
    Sorry for the "me too" but I really want to thanks you. I spend so much time stuck on this issue and becoming crazy. – Charles Walker Nov 26 '17 at 16:19
  • Even though this might work, there is a reason for having an `audio` or `video` element with multiple `source` child notes: In HTML5 you can define more than one file format/codec, so browsers can pick up the `source` that it supports best. Unfortunately, you loose that benefit, when defining the `ngScr` in your `audio` or `video` element. – user1438038 Jul 27 '22 at 14:07
5
<audio ng-src="{{getAudioUrl()}}" audioplayer controls></audio>

$scope.getAudioUrl = function() {
return $sce.trustAsResourceUrl('your url');
};

This is working for me you need to santize you

Rahul Shukla
  • 505
  • 7
  • 20
4

ngSrc doesn't work for video in AngularJS, only work for image. My solution is :

in the view :

        <video controls="controls" name="Video Name" ng-src="{{getVideoUrl()}}"></video>

in the controller:

  $scope.getVideoUrl=function(){
        return $sce.trustAsResourceUrl("media/"+$scope.groupedProjList[$scope.routeId].CONTACTNAME+".mp4");
   };

replace my Url with yours:

$sce.trustAsResourceUrl('your Url');

Don't forget to add $sce to your module:

angular.module('myApp.controllers', [])
  .controller('MyCtrl1',  function($scope,$http,$routeParams,$sce) {
Alexei - check Codidact
  • 22,016
  • 16
  • 145
  • 164
stackmave
  • 41
  • 2
2

I had a similar problem loading up video sources on with a dropdown select menu. I found that $scope.$apply(); was all I needed to add. Haven't had time to take your code and test this implementation, but I'd recommend trying out this:

$scope.songSelect = function(songPath) {
    $scope.selectedSongPath = songPath;
    $scope.$apply();
}

There's some dispute about the use of $scope.$apply() regarding handling errors. I believe the proper implementation is actually as follows:

$scope.songSelect = function(songPath) {
    $scope.$apply(function() {
        $scope.selectedSongPath = songPath;
    });
}

This supposedly copes better if songPath returns undefined; alas I can't find the link where I learnt this trick. If I do, I'll post it as a comment to this answer.

Hope this helps :)

Jeshua
  • 363
  • 1
  • 3
  • 15
1

You can also use this:

<div ng-bind-html-unsafe="audioPlayer"></div>

On your controller:

$scope.audioPlayer="<br /><audio controls><source src=\""+ audiosource + "\" type=\"audio/mpeg\"> Your browser does not support the audio element. </audio>"

Attention:

You should use this directive only if ngBindHtml directive is too restrictive and when you absolutely trust the source of the content you are binding to.

more here

Michael Schmidt
  • 9,090
  • 13
  • 56
  • 80
Shote
  • 11
  • 2
1

@SteveOC 64's approach deals with creating a new instance of the Audio. If you already have an Audio tag like

<audio id="audio" controls></audio>

You can try the below code

    app.factory('audio', function($document) {
    var audioElement = $document[0].getElementById('audio');
    audioElement.autoPlay = true; // as per your requirement

    return {
      audioElement: audioElement,

      play: function(filename) {
        audioElement.src = filename;
        audioElement.play();
      },
      resume: function() {
        audioElement.play();
      },
      pause: function() {
        audioElement.pause();
      },
      stop: function() {
        audioElement.pause();
        audioElement.src = audioElement.currentSrc; /** http://stackoverflow.com/a/16978083/1015046 **/
      },
      incVol: function() {
        if (audioElement.volume < 1) {
          audioElement.volume = (audioElement.volume + 0.1).toFixed(2);
        }
        return audioElement.volume;
      },
      decVol: function() {
        if (audioElement.volume > 0) {
          audioElement.volume = (audioElement.volume - 0.1).toFixed(2);
        }
        return audioElement.volume;
      },
      timer: function(callback) {
        audioElement.ontimeupdate = function() {
          callback(audioElement.duration, audioElement.currentTime)
        };
      },
    }
  });

And in your controller after injecting audio factory, you can call it like

 audio.play('/api/resource?resource=' + uri);

if this is a event driven, as in play audio on clicking another element

$scope.$apply(function() {
   $scope.audioPlayer = true;
   audio.play('/api/resource?resource=' + uri);
   $scope.isAudioPlaying = true;
});

And if someone is looking for the Video factory :

HTML

<video id="video" controls></video>

Factory

app.factory('video', function($document) {
       var videoElement = $document[0].getElementById('video');
       videoElement.autoPlay = true;
       return {
          videoElement: videoElement,
          play: function(filename) {
             videoElement.src = filename;
             videoElement.play();
          },
          resume: function() {
             videoElement.play();
          },
          pause: function() {
             videoElement.pause();
          },
          stop: function() {
             videoElement.pause();
             videoElement.src = videoElement.currentSrc; /** http://stackoverflow.com/a/16978083/1015046 **/
          },
          incVol: function() {
             if(videoElement.volume < 1) {
                videoElement.volume = (videoElement.volume + 0.1).toFixed(2);
             }
             return videoElement.volume;
          },
          decVol: function() {
             if(videoElement.volume > 0) {
                videoElement.volume = (videoElement.volume - 0.1).toFixed(2);
             }
             return videoElement.volume;
          },
          timer: function(callback) {
             videoElement.ontimeupdate = function() {
                callback(videoElement.duration, videoElement.currentTime)
             };
          },
       }
    });

And you can invoke it as video.play('/api/resource?resource=' + uri);

Hope this helps!

Arvind
  • 671
  • 13
  • 46
1

The solution I found is to load the video as soon as you get video url from ajax call.

var video = angular.element('#video_id');
video.load();
Pushkar Kathuria
  • 331
  • 2
  • 17
0

Never tried with audio tag, but src should be interpolated, so it should be between {{}}.

<audio controls="controls" preload="none">
  <source type="audio/mpeg" src="{{selectedSongPath}}"/>
</audio>

Have you already tried this?

Caio Cunha
  • 23,326
  • 6
  • 78
  • 74
  • Sorry, typo (I didn't copy/paste). Yes, it is between {{}}. Edited. Again, src is changing, the player doesn't respond @CaioToOn – user1639431 Mar 18 '13 at 20:12
0

After you change the src, you have to load that file too, so try to add to your function songselect after $scope.selectedSongPath = songPath; the following:

$scope.load();

I don't know what $scope is in your case, but the load() method should be executed inside the object of your audio tag. In simple JS it would look like that:

<audio id="audio-tag" controls="controls" preload="none"> //need the ID to get element
    <source type="audio/mpeg" src="{{selectedSongPath}}"/>
</audio>
....
$scope.selectedSongPath = songPath; //you're changing the src
document.getElementById("audio-tag").load(); //here you load the file (you need equivalent in your framework)

Hope it helps.

sobol6803
  • 420
  • 1
  • 5
  • 21
  • In my case the ctrl is the whole . Anyway, $scope.load() where you suggested raises an exception: TypeError: Object # has no method 'load'. @sobol6803 – user1639431 Mar 18 '13 at 20:25
  • Your simple js solution works though, thanks. Any angularish solution available..? And what do you mean by "you need equivalent in your framework"? @sobol6803 – user1639431 Mar 18 '13 at 20:33
  • I mean this `document.get...` is only workaround in your case. If you use angularjs you should find solution in this framework. So in this case my answer is only a tip for you, because i don't know this framework and i don't really have time to study it now, but the method in plain js is definitely `load` and you should look for it. Maybe, if your audio tag was defined like you defined $scope (by `ng-something`) and then use $audio.load() or something like that? It's a long shot, but i can't help more for now. Sorry. – sobol6803 Mar 18 '13 at 21:13
  • Thanks a lot, your help is highly appreciated, but it is only a workaround: when I use document.get... I need to click on a certain link twice in order that the player will load @sobol6803 – user1639431 Mar 18 '13 at 22:11
  • Using load() was very helpful.Thanks a lot! –  Aug 23 '15 at 03:21
0

I was using ngAudio earlier & facing issue of multiple audio's played at a time. When tried to use HTML5 audio tag, instead of having audio & source as 2 tags, only 1 tag solved my issue.

<audio controls ng-src='{{trusted_url }}'></audio>

Swapnil Chincholkar
  • 2,869
  • 3
  • 22
  • 22
0

The above answers are great. For a bit of fun though, I've included an answer below that shows how to record the audio then add it to the player (this works for Angular 14). You'll need to sanatize the URL as well.

<!-- html doc: -->
<button (click)="recordAudio()">record</button>
<audio id="player" [src]="audioURL" controls></audio>

// TS Doc:

export class audioPlayerDemo implements OnInit {
import { DomSanitizer, SafeResourceUrl, SafeStyle, SafeUrl } from '@angular/platform-browser';

  constructor(private sanitizer: DomSanitizer) {}

  audioSrc = new Audio();
  audioURL ? : SafeUrl;

  recordAudio() {
    navigator.mediaDevices
      .getUserMedia({
        audio: true,
        video: false
      })
      .then(
        (stream) => {
          console.log(stream)

          const mediaRecorder = new MediaRecorder(stream);
          mediaRecorder.start();
          const audioChunks: BlobPart[] | undefined = [];
          mediaRecorder.addEventListener("dataavailable", event => {
            audioChunks.push(event.data);
          });
          mediaRecorder.addEventListener("stop", () => {
            const audioBlob = new Blob(audioChunks);
            const audioUrl = URL.createObjectURL(audioBlob);
            this.audioSrc = new Audio(audioUrl);
            this.audioURL = this.sanitizer.bypassSecurityTrustUrl(`${audioUrl}`);
            // this.audioSrc.play();
          });
          setTimeout(() => {
            mediaRecorder.stop();
          }, 6000); // this will stop the recoding after 6 seconds.
        }
      );
  }