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.6KB

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