Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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