You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

audioRecorder.js 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. /* global MediaRecorder, MediaStream, webkitMediaStream */
  2. const RecordingResult = require('./recordingResult');
  3. /**
  4. * Possible audio formats MIME types
  5. */
  6. const AUDIO_WEBM = 'audio/webm'; // Supported in chrome
  7. const AUDIO_OGG = 'audio/ogg'; // Supported in firefox
  8. /**
  9. * A TrackRecorder object holds all the information needed for recording a
  10. * single JitsiTrack (either remote or local)
  11. * @param track The JitsiTrack the object is going to hold
  12. */
  13. const TrackRecorder = function(track) {
  14. // The JitsiTrack holding the stream
  15. this.track = track;
  16. // The MediaRecorder recording the stream
  17. this.recorder = null;
  18. // The array of data chunks recorded from the stream
  19. // acts as a buffer until the data is stored on disk
  20. this.data = null;
  21. // the name of the person of the JitsiTrack. This can be undefined and/or
  22. // not unique
  23. this.name = null;
  24. // the time of the start of the recording
  25. this.startTime = null;
  26. };
  27. /**
  28. * Starts the recording of a JitsiTrack in a TrackRecorder object.
  29. * This will also define the timestamp and try to update the name
  30. * @param trackRecorder the TrackRecorder to start
  31. */
  32. function startRecorder(trackRecorder) {
  33. if(trackRecorder.recorder === undefined) {
  34. throw new Error('Passed an object to startRecorder which is not a '
  35. + 'TrackRecorder object');
  36. }
  37. trackRecorder.recorder.start();
  38. trackRecorder.startTime = new Date();
  39. }
  40. /**
  41. * Stops the recording of a JitsiTrack in a TrackRecorder object.
  42. * This will also try to update the name
  43. * @param trackRecorder the TrackRecorder to stop
  44. */
  45. function stopRecorder(trackRecorder) {
  46. if(trackRecorder.recorder === undefined) {
  47. throw new Error('Passed an object to stopRecorder which is not a '
  48. + 'TrackRecorder object');
  49. }
  50. trackRecorder.recorder.stop();
  51. }
  52. /**
  53. * Creates a TrackRecorder object. Also creates the MediaRecorder and
  54. * data array for the trackRecorder.
  55. * @param track the JitsiTrack holding the audio MediaStream(s)
  56. */
  57. function instantiateTrackRecorder(track) {
  58. const trackRecorder = new TrackRecorder(track);
  59. // Create a new stream which only holds the audio track
  60. const originalStream = trackRecorder.track.getOriginalStream();
  61. const stream = createEmptyStream();
  62. originalStream.getAudioTracks().forEach(function(track) {
  63. stream.addTrack(track);
  64. });
  65. // Create the MediaRecorder
  66. trackRecorder.recorder = new MediaRecorder(stream,
  67. {mimeType: audioRecorder.fileType});
  68. // array for holding the recorder data. Resets it when
  69. // audio already has been recorder once
  70. trackRecorder.data = [];
  71. // function handling a dataEvent, e.g the stream gets new data
  72. trackRecorder.recorder.ondataavailable = function(dataEvent) {
  73. if(dataEvent.data.size > 0) {
  74. trackRecorder.data.push(dataEvent.data);
  75. }
  76. };
  77. return trackRecorder;
  78. }
  79. /**
  80. * Determines which kind of audio recording the browser supports
  81. * chrome supports "audio/webm" and firefox supports "audio/ogg"
  82. */
  83. function determineCorrectFileType() {
  84. if(MediaRecorder.isTypeSupported(AUDIO_WEBM)) {
  85. return AUDIO_WEBM;
  86. } else if(MediaRecorder.isTypeSupported(AUDIO_OGG)) {
  87. return AUDIO_OGG;
  88. }
  89. throw new Error('unable to create a MediaRecorder with the'
  90. + 'right mimetype!');
  91. }
  92. /**
  93. * main exported object of the file, holding all
  94. * relevant functions and variables for the outside world
  95. * @param jitsiConference the jitsiConference which this object
  96. * is going to record
  97. */
  98. const audioRecorder = function(jitsiConference) {
  99. // array of TrackRecorders, where each trackRecorder
  100. // holds the JitsiTrack, MediaRecorder and recorder data
  101. this.recorders = [];
  102. // get which file type is supported by the current browser
  103. this.fileType = determineCorrectFileType();
  104. // boolean flag for active recording
  105. this.isRecording = false;
  106. // the jitsiconference the object is recording
  107. this.jitsiConference = jitsiConference;
  108. };
  109. /**
  110. * Add the the exported module so that it can be accessed by other files
  111. */
  112. audioRecorder.determineCorrectFileType = determineCorrectFileType;
  113. /**
  114. * Adds a new TrackRecorder object to the array.
  115. *
  116. * @param track the track potentially holding an audio stream
  117. */
  118. audioRecorder.prototype.addTrack = function(track) {
  119. if(track.isAudioTrack()) {
  120. // create the track recorder
  121. const trackRecorder = instantiateTrackRecorder(track);
  122. // push it to the local array of all recorders
  123. this.recorders.push(trackRecorder);
  124. // update the name of the trackRecorders
  125. this.updateNames();
  126. // If we're already recording, immediately start recording this new
  127. // track.
  128. if(this.isRecording) {
  129. startRecorder(trackRecorder);
  130. }
  131. }
  132. };
  133. /**
  134. * Notifies the module that a specific track has stopped, e.g participant left
  135. * the conference.
  136. * if the recording has not started yet, the TrackRecorder will be removed from
  137. * the array. If the recording has started, the recorder will stop recording
  138. * but not removed from the array so that the recorded stream can still be
  139. * accessed
  140. *
  141. * @param {JitsiTrack} track the JitsiTrack to remove from the recording session
  142. */
  143. audioRecorder.prototype.removeTrack = function(track) {
  144. if(track.isVideoTrack()) {
  145. return;
  146. }
  147. const array = this.recorders;
  148. let i;
  149. for(i = 0; i < array.length; i++) {
  150. if(array[i].track.getParticipantId() === track.getParticipantId()) {
  151. const recorderToRemove = array[i];
  152. if(this.isRecording) {
  153. stopRecorder(recorderToRemove);
  154. } else {
  155. // remove the TrackRecorder from the array
  156. array.splice(i, 1);
  157. }
  158. }
  159. }
  160. // make sure the names are up to date
  161. this.updateNames();
  162. };
  163. /**
  164. * Tries to update the name value of all TrackRecorder in the array.
  165. * If it hasn't changed,it will keep the exiting name. If it changes to a
  166. * undefined value, the old value will also be kept.
  167. */
  168. audioRecorder.prototype.updateNames = function() {
  169. const conference = this.jitsiConference;
  170. this.recorders.forEach(function(trackRecorder) {
  171. if(trackRecorder.track.isLocal()) {
  172. trackRecorder.name = 'the transcriber';
  173. } else {
  174. const id = trackRecorder.track.getParticipantId();
  175. const participant = conference.getParticipantById(id);
  176. const newName = participant.getDisplayName();
  177. if(newName !== 'undefined') {
  178. trackRecorder.name = newName;
  179. }
  180. }
  181. });
  182. };
  183. /**
  184. * Starts the audio recording of every local and remote track
  185. */
  186. audioRecorder.prototype.start = function() {
  187. if(this.isRecording) {
  188. throw new Error('audiorecorder is already recording');
  189. }
  190. // set boolean isRecording flag to true so if new participants join the
  191. // conference, that track can instantly start recording as well
  192. this.isRecording = true;
  193. // start all the mediaRecorders
  194. this.recorders.forEach(function(trackRecorder) {
  195. startRecorder(trackRecorder);
  196. });
  197. // log that recording has started
  198. console.log('Started the recording of the audio. There are currently '
  199. + this.recorders.length + ' recorders active.');
  200. };
  201. /**
  202. * Stops the audio recording of every local and remote track
  203. */
  204. audioRecorder.prototype.stop = function() {
  205. // set the boolean flag to false
  206. this.isRecording = false;
  207. // stop all recorders
  208. this.recorders.forEach(function(trackRecorder) {
  209. stopRecorder(trackRecorder);
  210. });
  211. console.log('stopped recording');
  212. };
  213. /**
  214. * link hacking to download all recorded audio streams
  215. */
  216. audioRecorder.prototype.download = function() {
  217. const t = this;
  218. this.recorders.forEach(function(trackRecorder) {
  219. const blob = new Blob(trackRecorder.data, {type: t.fileType});
  220. const url = URL.createObjectURL(blob);
  221. const a = document.createElement('a');
  222. document.body.appendChild(a);
  223. a.style = 'display: none';
  224. a.href = url;
  225. a.download = 'test.' + t.fileType.split('/')[1];
  226. a.click();
  227. window.URL.revokeObjectURL(url);
  228. });
  229. };
  230. /**
  231. * returns the audio files of all recorders as an array of objects,
  232. * which include the name of the owner of the track and the starting time stamp
  233. * @returns {Array} an array of RecordingResult objects
  234. */
  235. audioRecorder.prototype.getRecordingResults = function() {
  236. if(this.isRecording) {
  237. throw new Error('cannot get blobs because the AudioRecorder is still'
  238. + 'recording!');
  239. }
  240. // make sure the names are up to date before sending them off
  241. this.updateNames();
  242. const array = [];
  243. const t = this;
  244. this.recorders.forEach(function(recorder) {
  245. array.push(
  246. new RecordingResult(
  247. new Blob(recorder.data, {type: t.fileType}),
  248. recorder.name,
  249. recorder.startTime)
  250. );
  251. });
  252. return array;
  253. };
  254. /**
  255. * Gets the mime type of the recorder audio
  256. * @returns {String} the mime type of the recorder audio
  257. */
  258. audioRecorder.prototype.getFileType = function() {
  259. return this.fileType;
  260. };
  261. /**
  262. * Creates a empty MediaStream object which can be used
  263. * to add MediaStreamTracks to
  264. * @returns MediaStream
  265. */
  266. function createEmptyStream() {
  267. // Firefox supports the MediaStream object, Chrome webkitMediaStream
  268. if(typeof MediaStream !== 'undefined') {
  269. return new MediaStream();
  270. } else if(typeof webkitMediaStream !== 'undefined') {
  271. return new webkitMediaStream(); // eslint-disable-line new-cap
  272. }
  273. throw new Error('cannot create a clean mediaStream');
  274. }
  275. /**
  276. * export the main object audioRecorder
  277. */
  278. module.exports = audioRecorder;