<commit_before>/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Subscription } from "rxjs/Subscription";
import { BehaviorSubject } from "rxjs/BehaviorSubject";
import { combineLatest } from "rxjs/observable/combineLatest";
import { only } from "../utils/rx-utils";
import { findBetterMatchIndex } from "../utils/languages";

import AverageBitrate from "./average-bitrate";

const DEFAULTS = {
  defaultAudioTrack: {
    language: "fra",
    audioDescription: false,
  },
  defaultTextTrack: null,
  // default buffer size in seconds
  defaultBufferSize: 30,
  // buffer threshold ratio used as a lower bound
  // margin to find the suitable representation
  defaultBufferThreshold: 0.3,
  initialVideoBitrate: 0,
  initialAudioBitrate: 0,
  maxVideoBitrate: Infinity,
  maxAudioBitrate: Infinity,
};

/**
 * Simple find function implementation.
 * @param {Array} array
 * @param {Function} predicate - The predicate. Will take as arguments:
 *   1. the current array element
 *   2. the array index
 *   3. the entire array
 * @returns {*} - null if not found
 */
function find(array, predicate) {
  for (let i = 0; i < array.length; i++) {
    if (predicate(array[i], i, array) === true) {
      return array[i];
    }
  }
  return null;
}

/**
 * Returns val if x is either not a Number type or inferior or equal to 0.
 * @param {Number} [x]
 * @param {*} val
 * @returns {*}
 */
function def(x, val) {
  return typeof x == "number" && x > 0 ? x : val;
}

/**
 * Get closest bitrate lower or equal to the bitrate wanted when the threshold
 * is equal to 0. You can add a security margin by setting the threshold between
 * 0 and 1.
 * @param {Array.<Number>} bitrates - all available bitrates.
 * @param {Number} btr - a chosen bitrate
 * @param {Number} [treshold=0]
 * @returns {Number}
 */
function getClosestBitrate(bitrates, btr, threshold=0) {
  for (let i = bitrates.length - 1; i >= 0; i--) {
    if ((bitrates[i] / btr) <= (1 - threshold)) {
      return bitrates[i];
    }
  }
  return bitrates[0];
}

/**
 * Get the highest bitrate from the representations having a width immediately
 * superior or equal to the given one.
 * @param {Array.<Object>} reps - The representations array
 * @param {Number} width
 * @returns {Number}
 */
function getMaxUsefulBitrateforWidth(reps, width) {
  const sortedRepsByWidth = reps.sort((a, b) => a.width - b.width);
  const maxWidth = sortedRepsByWidth.find(r => r.width >= width);

  if (maxWidth) {
    const filteredAdaptations = reps.filter(r => r.width <= maxWidth);
    if (filteredAdaptations.length) {
      return filteredAdaptations[filteredAdaptations.length - 1].bitrate;
    }
  }

  return Infinity;
}

/**
 * Find first adaptation with the corresponding language.
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @returns {Object|null}
 */
function findAdaptationByLang(adaptations, language) {
  const languages = adaptations.map(a => a.language);

  const index = findBetterMatchIndex(languages, language);
  if (index >= 0) {
    return adaptations[index];
  }
  return null;
}

/**
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @param {Boolean} [audioDescription=false]
 * @returns {Object|null}
 */
function findAudioAdaptation(adaptations, language, audioDescription = false) {
  const filteredAdaptations = adaptations.filter(adaptation =>
    adaptation.isAudioDescription == audioDescription
  );
  return findAdaptationByLang(filteredAdaptations, language);
}

/**
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @param {Boolean} [closedCaption=false]
 * @returns {Object|null}
 */
function findTextAdaptation(adaptations, language, closedCaption = false) {
  const filteredAdaptations = adaptations.filter(adaptation =>
    adaptation.isClosedCaption == closedCaption
  );
  return findAdaptationByLang(filteredAdaptations, language);
}

/**
 * Filter the given observable/array to only keep the item with the selected
 * type.
 * @param {Observable|Array.<Object>} stream
 * @param {string} selectedType
 * @returns {Observable|Array.<Object>}
 */
function filterByType(stream, selectedType) {
  return stream.filter(({ type }) => type === selectedType);
}

export default function(metrics, deviceEvents, options={}) {
  Object.keys(options).forEach(key =>
    options[key] === undefined && delete options[key]
  );

  const {
    defaultAudioTrack,
    defaultTextTrack,
    defaultBufferSize,
    defaultBufferThreshold,
    initialVideoBitrate,
    initialAudioBitrate,
    maxVideoBitrate,
    maxAudioBitrate,
    limitVideoWidth,
    throttleWhenHidden,
  } = Object.assign({}, DEFAULTS, options);

  const { videoWidth, inBackground } = deviceEvents;

  const $languages = new BehaviorSubject(defaultAudioTrack);
  const $subtitles = new BehaviorSubject(defaultTextTrack);

  const $averageBitrates = {
    audio: new BehaviorSubject(initialAudioBitrate),
    video: new BehaviorSubject(initialVideoBitrate),
  };

  const averageBitratesConns = [
    AverageBitrate(filterByType(metrics, "audio"), { alpha: 0.6 })
      .multicast($averageBitrates.audio),
    AverageBitrate(filterByType(metrics, "video"), { alpha: 0.6 })
      .multicast($averageBitrates.video),
  ];

  let conns = new Subscription();
  averageBitratesConns.forEach((a) => conns.add(a.connect()));

  const $usrBitrates = {
    audio: new BehaviorSubject(Infinity),
    video: new BehaviorSubject(Infinity),
  };

  const $maxBitrates = {
    audio: new BehaviorSubject(maxAudioBitrate),
    video: new BehaviorSubject(maxVideoBitrate),
  };

  const $bufSizes = {
    audio: new BehaviorSubject(defaultBufferSize),
    video: new BehaviorSubject(defaultBufferSize),
    text:  new BehaviorSubject(defaultBufferSize),
  };

  /**
   * Returns an Observable emitting:
   *   - first, the current audio adaption
   *   - the new one each time it changes
   * @param {Array.<Object>} adaptations - The available audio adaptations
   * objects.
   * @returns {Observable}
   */
  function audioAdaptationChoice(adaptations) {
    return $languages.distinctUntilChanged()
      .map(({ language, audioDescription }) =>
        findAudioAdaptation(
          adaptations,
          language,
          audioDescription
        ) || adaptations[0]
      );
  }

  /**
   * Returns an Observable emitting:
   *   - first, the current text adaption
   *   - the new one each time it changes
   * @param {Array.<Object>} adaptations - The available text adaptations
   * objects.
   * @returns {Observable}
   */
  function textAdaptationChoice(adaptations) {
    return $subtitles.distinctUntilChanged()
      .map(arg =>
        arg ?  findTextAdaptation(
          adaptations,
          arg.language,
          arg.closedCaption
        ) : null
      );
  }

  /**
   * Get the current and new adaptations each time it changes for all
   * types.
   * Mostly useful for audio languages and text subtitles to know which one
   * to choose first and when it changes.
   * @param {string} type - The adaptation type
   * @param {Array.<Object>} adaptations
   * @returns {Observable}
   */
  function getAdaptationsChoice(type, adaptations) {
    if (type == "audio") {
      return audioAdaptationChoice(adaptations);
    }

    if (type == "text") {
      return textAdaptationChoice(adaptations);
    }

    return only(adaptations[0]);
  }

  /**
   * Returns an object containing two observables:
   *   - representations: the chosen best representation for the adaptation
   *     (correlated from the user, max and average bitrates)
   *   - bufferSizes: the bufferSize chosen
   * @param {Object} adaptation
   * @returns {Object}
   */
  function getBufferAdapters(adaptation) {
    const { type, representations } = adaptation;
    const bitrates = adaptation.getAvailableBitrates();

    let representationsObservable;
    if (representations.length > 1) {
      const usrBitrates = $usrBitrates[type];
      let maxBitrates = $maxBitrates[type];

      const avrBitrates = $averageBitrates[type]
        .map((avrBitrate, count) => {
          // no threshold for the first value of the average bitrate
          // stream corresponding to the selected initial video bitrate
          let bufThreshold;
          if (count === 0) {
            bufThreshold = 0;
          } else {
            bufThreshold = defaultBufferThreshold;
          }

          return getClosestBitrate(bitrates, avrBitrate, bufThreshold);
        })
        .distinctUntilChanged()
        .debounceTime(2000)
        .startWith(getClosestBitrate(bitrates, $averageBitrates[type].getValue(), 0));

      if (type == "video") {
        // To compute the bitrate upper-bound for video
        // representations we need to combine multiple stream:
        //   - user-based maximum bitrate (subject)
        //   - maximum based on the video element width
        //   - maximum based on the application visibility (background tab)
        maxBitrates = combineLatest([maxBitrates, videoWidth, inBackground])
          .map(([maxSetBitrate, width, isHidden]) => {
            if (throttleWhenHidden && isHidden) {
              return bitrates[0];
            }

            const maxUsableBitrate = limitVideoWidth ?
              getMaxUsefulBitrateforWidth(representations, width) :
              bitrates[bitrates.length - 1];

            return Math.min(maxUsableBitrate, maxSetBitrate);
          });
      }

      representationsObservable = combineLatest(
        usrBitrates,
        maxBitrates,
        avrBitrates,
        (usr, max, avr) => {
          let btr;
          if (usr < Infinity) {
            btr = usr;
          } else if (max < Infinity) {
            btr = Math.min(max, avr);
          } else {
            btr = avr;
          }
          return find(representations, (rep) => rep.bitrate === getClosestBitrate(bitrates, btr));
        })
        .distinctUntilChanged((a, b) => a.id === b.id);
    }
    else { // representations.length <= 1
      representationsObservable = only(representations[0]);
    }

    return {
      representations: representationsObservable,
      bufferSizes: $bufSizes[type] || new BehaviorSubject(defaultBufferSize),
    };
  }

  return {
    setAudioTrack(track) { $languages.next(track); },
    setTextTrack(track)  { $subtitles.next(track); },
    getAudioTrack()      { return $languages.getValue(); },
    getTextTrack()       { return $subtitles.getValue(); },

    getAverageBitrates() { return $averageBitrates; },

    getAudioMaxBitrate() { return $maxBitrates.audio.getValue(); },
    getVideoMaxBitrate() { return $maxBitrates.video.getValue(); },
    getAudioBufferSize() { return $bufSizes.audio.getValue(); },
    getVideoBufferSize() { return $bufSizes.video.getValue(); },

    setAudioBitrate(x)    { $usrBitrates.audio.next(def(x, Infinity)); },
    setVideoBitrate(x)    { $usrBitrates.video.next(def(x, Infinity)); },
    setAudioMaxBitrate(x) { $maxBitrates.audio.next(def(x, Infinity)); },
    setVideoMaxBitrate(x) { $maxBitrates.video.next(def(x, Infinity)); },
    setAudioBufferSize(x) { $bufSizes.audio.next(def(x, defaultBufferSize)); },
    setVideoBufferSize(x) { $bufSizes.video.next(def(x, defaultBufferSize)); },

    getBufferAdapters,
    getAdaptationsChoice,

    unsubscribe() {
      if (conns) {
        conns.unsubscribe();
        conns = null;
      }
    },
  };
}
<commit_msg>adaptive: take the lowest bitrate when the player is too small or not displayed<commit_after>/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Subscription } from "rxjs/Subscription";
import { BehaviorSubject } from "rxjs/BehaviorSubject";
import { combineLatest } from "rxjs/observable/combineLatest";
import { only } from "../utils/rx-utils";
import { findBetterMatchIndex } from "../utils/languages";

import AverageBitrate from "./average-bitrate";

const DEFAULTS = {
  defaultAudioTrack: {
    language: "fra",
    audioDescription: false,
  },
  defaultTextTrack: null,
  // default buffer size in seconds
  defaultBufferSize: 30,
  // buffer threshold ratio used as a lower bound
  // margin to find the suitable representation
  defaultBufferThreshold: 0.3,
  initialVideoBitrate: 0,
  initialAudioBitrate: 0,
  maxVideoBitrate: Infinity,
  maxAudioBitrate: Infinity,
};

/**
 * Simple find function implementation.
 * @param {Array} array
 * @param {Function} predicate - The predicate. Will take as arguments:
 *   1. the current array element
 *   2. the array index
 *   3. the entire array
 * @returns {*} - null if not found
 */
function find(array, predicate) {
  for (let i = 0; i < array.length; i++) {
    if (predicate(array[i], i, array) === true) {
      return array[i];
    }
  }
  return null;
}

/**
 * Returns val if x is either not a Number type or inferior or equal to 0.
 * @param {Number} [x]
 * @param {*} val
 * @returns {*}
 */
function def(x, val) {
  return typeof x == "number" && x > 0 ? x : val;
}

/**
 * Get closest bitrate lower or equal to the bitrate wanted when the threshold
 * is equal to 0. You can add a security margin by setting the threshold between
 * 0 and 1.
 * @param {Array.<Number>} bitrates - all available bitrates.
 * @param {Number} btr - a chosen bitrate
 * @param {Number} [treshold=0]
 * @returns {Number}
 */
function getClosestBitrate(bitrates, btr, threshold=0) {
  for (let i = bitrates.length - 1; i >= 0; i--) {
    if ((bitrates[i] / btr) <= (1 - threshold)) {
      return bitrates[i];
    }
  }
  return bitrates[0];
}

/**
 * Get the highest bitrate from the representations having a width immediately
 * superior or equal to the given one.
 * @param {Array.<Object>} representations - The representations array
 * @param {Number} width
 * @returns {Number}
 */
function getMaxUsefulBitrateforWidth(representations, width) {
  const sortedRepsByWidth = representations.sort((a, b) => a.width - b.width);
  const maxWidth = sortedRepsByWidth.find(r => r.width >= width);

  if (maxWidth) {
    const filteredAdaptations = representations.filter(r => r.width <= maxWidth);
    if (filteredAdaptations.length) {
      return filteredAdaptations[filteredAdaptations.length - 1].bitrate;
    } else {
      return representations[0];
    }
  }

  return Infinity;
}

/**
 * Find first adaptation with the corresponding language.
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @returns {Object|null}
 */
function findAdaptationByLang(adaptations, language) {
  const languages = adaptations.map(a => a.language);

  const index = findBetterMatchIndex(languages, language);
  if (index >= 0) {
    return adaptations[index];
  }
  return null;
}

/**
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @param {Boolean} [audioDescription=false]
 * @returns {Object|null}
 */
function findAudioAdaptation(adaptations, language, audioDescription = false) {
  const filteredAdaptations = adaptations.filter(adaptation =>
    adaptation.isAudioDescription == audioDescription
  );
  return findAdaptationByLang(filteredAdaptations, language);
}

/**
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @param {Boolean} [closedCaption=false]
 * @returns {Object|null}
 */
function findTextAdaptation(adaptations, language, closedCaption = false) {
  const filteredAdaptations = adaptations.filter(adaptation =>
    adaptation.isClosedCaption == closedCaption
  );
  return findAdaptationByLang(filteredAdaptations, language);
}

/**
 * Filter the given observable/array to only keep the item with the selected
 * type.
 * @param {Observable|Array.<Object>} stream
 * @param {string} selectedType
 * @returns {Observable|Array.<Object>}
 */
function filterByType(stream, selectedType) {
  return stream.filter(({ type }) => type === selectedType);
}

export default function(metrics, deviceEvents, options={}) {
  Object.keys(options).forEach(key =>
    options[key] === undefined && delete options[key]
  );

  const {
    defaultAudioTrack,
    defaultTextTrack,
    defaultBufferSize,
    defaultBufferThreshold,
    initialVideoBitrate,
    initialAudioBitrate,
    maxVideoBitrate,
    maxAudioBitrate,
    limitVideoWidth,
    throttleWhenHidden,
  } = Object.assign({}, DEFAULTS, options);

  const { videoWidth, inBackground } = deviceEvents;

  const $languages = new BehaviorSubject(defaultAudioTrack);
  const $subtitles = new BehaviorSubject(defaultTextTrack);

  const $averageBitrates = {
    audio: new BehaviorSubject(initialAudioBitrate),
    video: new BehaviorSubject(initialVideoBitrate),
  };

  const averageBitratesConns = [
    AverageBitrate(filterByType(metrics, "audio"), { alpha: 0.6 })
      .multicast($averageBitrates.audio),
    AverageBitrate(filterByType(metrics, "video"), { alpha: 0.6 })
      .multicast($averageBitrates.video),
  ];

  let conns = new Subscription();
  averageBitratesConns.forEach((a) => conns.add(a.connect()));

  const $usrBitrates = {
    audio: new BehaviorSubject(Infinity),
    video: new BehaviorSubject(Infinity),
  };

  const $maxBitrates = {
    audio: new BehaviorSubject(maxAudioBitrate),
    video: new BehaviorSubject(maxVideoBitrate),
  };

  const $bufSizes = {
    audio: new BehaviorSubject(defaultBufferSize),
    video: new BehaviorSubject(defaultBufferSize),
    text:  new BehaviorSubject(defaultBufferSize),
  };

  /**
   * Returns an Observable emitting:
   *   - first, the current audio adaption
   *   - the new one each time it changes
   * @param {Array.<Object>} adaptations - The available audio adaptations
   * objects.
   * @returns {Observable}
   */
  function audioAdaptationChoice(adaptations) {
    return $languages.distinctUntilChanged()
      .map(({ language, audioDescription }) =>
        findAudioAdaptation(
          adaptations,
          language,
          audioDescription
        ) || adaptations[0]
      );
  }

  /**
   * Returns an Observable emitting:
   *   - first, the current text adaption
   *   - the new one each time it changes
   * @param {Array.<Object>} adaptations - The available text adaptations
   * objects.
   * @returns {Observable}
   */
  function textAdaptationChoice(adaptations) {
    return $subtitles.distinctUntilChanged()
      .map(arg =>
        arg ?  findTextAdaptation(
          adaptations,
          arg.language,
          arg.closedCaption
        ) : null
      );
  }

  /**
   * Get the current and new adaptations each time it changes for all
   * types.
   * Mostly useful for audio languages and text subtitles to know which one
   * to choose first and when it changes.
   * @param {string} type - The adaptation type
   * @param {Array.<Object>} adaptations
   * @returns {Observable}
   */
  function getAdaptationsChoice(type, adaptations) {
    if (type == "audio") {
      return audioAdaptationChoice(adaptations);
    }

    if (type == "text") {
      return textAdaptationChoice(adaptations);
    }

    return only(adaptations[0]);
  }

  /**
   * Returns an object containing two observables:
   *   - representations: the chosen best representation for the adaptation
   *     (correlated from the user, max and average bitrates)
   *   - bufferSizes: the bufferSize chosen
   * @param {Object} adaptation
   * @returns {Object}
   */
  function getBufferAdapters(adaptation) {
    const { type, representations } = adaptation;
    const bitrates = adaptation.getAvailableBitrates();

    let representationsObservable;
    if (representations.length > 1) {
      const usrBitrates = $usrBitrates[type];
      let maxBitrates = $maxBitrates[type];

      const avrBitrates = $averageBitrates[type]
        .map((avrBitrate, count) => {
          // no threshold for the first value of the average bitrate
          // stream corresponding to the selected initial video bitrate
          let bufThreshold;
          if (count === 0) {
            bufThreshold = 0;
          } else {
            bufThreshold = defaultBufferThreshold;
          }

          return getClosestBitrate(bitrates, avrBitrate, bufThreshold);
        })
        .distinctUntilChanged()
        .debounceTime(2000)
        .startWith(getClosestBitrate(bitrates, $averageBitrates[type].getValue(), 0));

      if (type == "video") {
        // To compute the bitrate upper-bound for video
        // representations we need to combine multiple stream:
        //   - user-based maximum bitrate (subject)
        //   - maximum based on the video element width
        //   - maximum based on the application visibility (background tab)
        maxBitrates = combineLatest([maxBitrates, videoWidth, inBackground])
          .map(([maxSetBitrate, width, isHidden]) => {
            if (throttleWhenHidden && isHidden) {
              return bitrates[0];
            }

            const maxUsableBitrate = limitVideoWidth ?
              getMaxUsefulBitrateforWidth(representations, width) :
              bitrates[bitrates.length - 1];

            return Math.min(maxUsableBitrate, maxSetBitrate);
          });
      }

      representationsObservable = combineLatest(
        usrBitrates,
        maxBitrates,
        avrBitrates,
        (usr, max, avr) => {
          let btr;
          if (usr < Infinity) {
            btr = usr;
          } else if (max < Infinity) {
            btr = Math.min(max, avr);
          } else {
            btr = avr;
          }
          return find(representations, (rep) => rep.bitrate === getClosestBitrate(bitrates, btr));
        })
        .distinctUntilChanged((a, b) => a.id === b.id);
    }
    else { // representations.length <= 1
      representationsObservable = only(representations[0]);
    }

    return {
      representations: representationsObservable,
      bufferSizes: $bufSizes[type] || new BehaviorSubject(defaultBufferSize),
    };
  }

  return {
    setAudioTrack(track) { $languages.next(track); },
    setTextTrack(track)  { $subtitles.next(track); },
    getAudioTrack()      { return $languages.getValue(); },
    getTextTrack()       { return $subtitles.getValue(); },

    getAverageBitrates() { return $averageBitrates; },

    getAudioMaxBitrate() { return $maxBitrates.audio.getValue(); },
    getVideoMaxBitrate() { return $maxBitrates.video.getValue(); },
    getAudioBufferSize() { return $bufSizes.audio.getValue(); },
    getVideoBufferSize() { return $bufSizes.video.getValue(); },

    setAudioBitrate(x)    { $usrBitrates.audio.next(def(x, Infinity)); },
    setVideoBitrate(x)    { $usrBitrates.video.next(def(x, Infinity)); },
    setAudioMaxBitrate(x) { $maxBitrates.audio.next(def(x, Infinity)); },
    setVideoMaxBitrate(x) { $maxBitrates.video.next(def(x, Infinity)); },
    setAudioBufferSize(x) { $bufSizes.audio.next(def(x, defaultBufferSize)); },
    setVideoBufferSize(x) { $bufSizes.video.next(def(x, defaultBufferSize)); },

    getBufferAdapters,
    getAdaptationsChoice,

    unsubscribe() {
      if (conns) {
        conns.unsubscribe();
        conns = null;
      }
    },
  };
}
<|endoftext|>


<commit_before>var Service, Characteristic;

const Co2Monitor = require("co2-monitor");
let co2Monitor = new Co2Monitor();

module.exports = function(homebridge) {
  Service = homebridge.hap.Service;
  Characteristic = homebridge.hap.Characteristic;
  homebridge.registerAccessory(
    "homebridge-aircontrol-mini",
    "aircontrol-mini",
    CO2Accessory
  );
};

function CO2Accessory(log, config) {
  var that = this;

  this.log = log;
  this.name = config["name"];
  this.co2_warning_level = config["co2_warning_level"];
  if (typeof(this.co2_warning_level) != "number") {
      this.co2_warning_level = 1200; // the manual says the RED level is at 1200ppm
  }

  // Set up CO2
  let monitor = co2Monitor;
  this.co2monitor = monitor;
  this.co2service = new Service.CarbonDioxideSensor(this.name);

  this.co2monitor.on("co2", co2 => {
    that.log(that.name, "CO2 (ppm):", co2);
    if (!that.co2_peak || co2 > that.co2_peak) {
      that.log(that.name, "CO2 Peak (ppm):", co2);
      that.co2_peak = co2;
      that.co2service.setCharacteristic(Characteristic.CarbonDioxidePeakLevel, co2);
    }
    that.co2 = co2;

    var result = that.co2 > that.co2_warning_level
                    ? Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL
                    : Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL;

    that.co2service.setCharacteristic(Characteristic.CarbonDioxideLevel, co2);
    that.co2service.setCharacteristic(Characteristic.CarbonDioxideDetected, result);
  });

  this.informationService = new Service.AccessoryInformation();
  this.informationService.setCharacteristic(
    Characteristic.Manufacturer,
    "TFA-Dostmann"
  );
  this.informationService.setCharacteristic(
    Characteristic.Model,
    "AirControl Mini"
  );

  this.co2service
    .getCharacteristic(Characteristic.CarbonDioxideLevel)
    .on("get", this.getLevel.bind(this));

  this.co2service
    .getCharacteristic(Characteristic.CarbonDioxideDetected)
    .on("get", this.getDetected.bind(this));

  this.co2service
    .getCharacteristic(Characteristic.CarbonDioxidePeakLevel)
    .on("get", this.getPeakLevel.bind(this));

  // Set up Temperature Service
  this.temperatureService = new Service.TemperatureSensor(this.name);

  this.co2monitor.on("temp", temp => {
    that.log(that.name, "Temperatue (C):", temp);
    that.temperature = temp;
    that.temperatureService.setCharacteristic(Characteristic.CurrentTemperature, new Number(temp));
  });

  this.temperatureService
    .getCharacteristic(Characteristic.CurrentTemperature)
    .on("get", this.getCurrentTemperature.bind(this));

  // Connection
  this.co2monitor.on("connected", device => {
    monitor.startTransfer();
    that.log(that.name, "Connected, recieving data…");
  });

  this.co2monitor.connect();
}

CO2Accessory.prototype.getLevel = function(callback) {
  if (this.co2 == null || this.co2 == undefined) {
    callback(null, null);
    return;
  }
  callback(null, this.co2);
};

CO2Accessory.prototype.getPeakLevel = function(callback) {
  if (this.co2_peak == null || this.co2_peak == undefined) {
    callback(null, null);
    return;
  }
  callback(null, this.co2_peak);
};

CO2Accessory.prototype.getDetected = function(callback) {
  if (this.co2 == null || this.co2 == undefined) {
    callback(null, null);
    return;
  }
  var result = this.co2 > this.co2_warning_level
    ? Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL
    : Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL;
  callback(null, result);
};

CO2Accessory.prototype.getCurrentTemperature = function(callback) {
  if (this.temperature == null || this.temperature == undefined) {
    callback(null, null);
    return;
  }
  callback(null, new Number(this.temperature));
};

CO2Accessory.prototype.getServices = function() {
  return [this.co2service, this.temperatureService, this.informationService];
};
<commit_msg>Fix fractional temperatures<commit_after>var Service, Characteristic;

const Co2Monitor = require("co2-monitor");
let co2Monitor = new Co2Monitor();

module.exports = function(homebridge) {
  Service = homebridge.hap.Service;
  Characteristic = homebridge.hap.Characteristic;
  homebridge.registerAccessory(
    "homebridge-aircontrol-mini",
    "aircontrol-mini",
    CO2Accessory
  );
};

function CO2Accessory(log, config) {
  var that = this;

  this.log = log;
  this.name = config["name"];
  this.co2_warning_level = config["co2_warning_level"];
  if (typeof(this.co2_warning_level) != "number") {
      this.co2_warning_level = 1200; // the manual says the RED level is at 1200ppm
  }

  // Set up CO2
  let monitor = co2Monitor;
  this.co2monitor = monitor;
  this.co2service = new Service.CarbonDioxideSensor(this.name);

  this.co2monitor.on("co2", co2 => {
    that.log(that.name, "CO2 (ppm):", co2);
    if (!that.co2_peak || co2 > that.co2_peak) {
      that.log(that.name, "CO2 Peak (ppm):", co2);
      that.co2_peak = co2;
      that.co2service.setCharacteristic(Characteristic.CarbonDioxidePeakLevel, co2);
    }
    that.co2 = co2;

    var result = that.co2 > that.co2_warning_level
                    ? Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL
                    : Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL;

    that.co2service.setCharacteristic(Characteristic.CarbonDioxideLevel, co2);
    that.co2service.setCharacteristic(Characteristic.CarbonDioxideDetected, result);
  });

  this.informationService = new Service.AccessoryInformation();
  this.informationService.setCharacteristic(
    Characteristic.Manufacturer,
    "TFA-Dostmann"
  );
  this.informationService.setCharacteristic(
    Characteristic.Model,
    "AirControl Mini"
  );

  this.co2service
    .getCharacteristic(Characteristic.CarbonDioxideLevel)
    .on("get", this.getLevel.bind(this));

  this.co2service
    .getCharacteristic(Characteristic.CarbonDioxideDetected)
    .on("get", this.getDetected.bind(this));

  this.co2service
    .getCharacteristic(Characteristic.CarbonDioxidePeakLevel)
    .on("get", this.getPeakLevel.bind(this));

  // Set up Temperature Service
  this.temperatureService = new Service.TemperatureSensor(this.name);

  this.co2monitor.on("temp", temp => {
    that.log(that.name, "Temperatue (C):", temp);
    that.temperature = temp;
    that.temperatureService.setCharacteristic(Characteristic.CurrentTemperature, Number.parseFloat(temp));
  });

  this.temperatureService
    .getCharacteristic(Characteristic.CurrentTemperature)
    .on("get", this.getCurrentTemperature.bind(this));

  // Connection
  this.co2monitor.on("connected", device => {
    monitor.startTransfer();
    that.log(that.name, "Connected, recieving data…");
  });

  this.co2monitor.connect();
}

CO2Accessory.prototype.getLevel = function(callback) {
  if (this.co2 == null || this.co2 == undefined) {
    callback(null, null);
    return;
  }
  callback(null, this.co2);
};

CO2Accessory.prototype.getPeakLevel = function(callback) {
  if (this.co2_peak == null || this.co2_peak == undefined) {
    callback(null, null);
    return;
  }
  callback(null, this.co2_peak);
};

CO2Accessory.prototype.getDetected = function(callback) {
  if (this.co2 == null || this.co2 == undefined) {
    callback(null, null);
    return;
  }
  var result = this.co2 > this.co2_warning_level
    ? Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL
    : Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL;
  callback(null, result);
};

CO2Accessory.prototype.getCurrentTemperature = function(callback) {
  if (this.temperature == null || this.temperature == undefined) {
    callback(null, null);
    return;
  }
  callback(null, new Number(this.temperature));
};

CO2Accessory.prototype.getServices = function() {
  return [this.co2service, this.temperatureService, this.informationService];
};
<|endoftext|>


<commit_before>/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Subscription } from "rxjs/Subscription";
import { BehaviorSubject } from "rxjs/BehaviorSubject";
import { combineLatest } from "rxjs/observable/combineLatest";
import { only } from "../utils/rx-utils";
import { findBetterMatchIndex } from "../utils/languages";

import AverageBitrate from "./average-bitrate";

const DEFAULTS = {
  defaultAudioTrack: {
    language: "fra",
    audioDescription: false,
  },
  defaultTextTrack: null,
  // default buffer size in seconds
  defaultBufferSize: 30,
  // buffer threshold ratio used as a lower bound
  // margin to find the suitable representation
  defaultBufferThreshold: 0.3,
  initialVideoBitrate: 0,
  initialAudioBitrate: 0,
  maxVideoBitrate: Infinity,
  maxAudioBitrate: Infinity,
};

/**
 * Simple find function implementation.
 * @param {Array} array
 * @param {Function} predicate - The predicate. Will take as arguments:
 *   1. the current array element
 *   2. the array index
 *   3. the entire array
 * @returns {*} - null if not found
 */
function find(array, predicate) {
  for (let i = 0; i < array.length; i++) {
    if (predicate(array[i], i, array) === true) {
      return array[i];
    }
  }
  return null;
}

/**
 * Returns val if x is either not a Number type or inferior or equal to 0.
 * @param {Number} [x]
 * @param {*} val
 * @returns {*}
 */
function def(x, val) {
  return typeof x == "number" && x > 0 ? x : val;
}

/**
 * Get closest bitrate lower or equal to the bitrate wanted when the threshold
 * is equal to 0. You can add a security margin by setting the threshold between
 * 0 and 1.
 * @param {Array.<Number>} bitrates - all available bitrates.
 * @param {Number} btr - a chosen bitrate
 * @param {Number} [treshold=0]
 * @returns {Number}
 */
function getClosestBitrate(bitrates, btr, threshold=0) {
  for (let i = bitrates.length - 1; i >= 0; i--) {
    if ((bitrates[i] / btr) <= (1 - threshold)) {
      return bitrates[i];
    }
  }
  return bitrates[0];
}

/**
 * Get the highest bitrate from the representations having a width immediately
 * superior or equal to the given one.
 * @param {Array.<Object>} representations - The representations array
 * @param {Number} width
 * @returns {Number}
 */
function getMaxUsefulBitrateforWidth(representations, width) {
  const sortedRepsByWidth = representations.sort((a, b) => a.width - b.width);
  const firstSuperiorRepresentation =
    sortedRepsByWidth.find(r => r.width >= width);

  if (firstSuperiorRepresentation) {
    const filteredAdaptations = representations
      .filter(r => r.width <= firstSuperiorRepresentation.width);
    if (filteredAdaptations.length) {
      return filteredAdaptations[filteredAdaptations.length - 1].bitrate;
    } else {
      const firstRepresentation = representations[0];
      return firstRepresentation ? firstRepresentation.bitrate : void 0;
    }
  }

  return Infinity;
}

/**
 * Find first adaptation with the corresponding language.
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @returns {Object|null}
 */
function findAdaptationByLang(adaptations, language) {
  const languages = adaptations.map(a => a.language);

  const index = findBetterMatchIndex(languages, language);
  if (index >= 0) {
    return adaptations[index];
  }
  return null;
}

/**
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @param {Boolean} [audioDescription=false]
 * @returns {Object|null}
 */
function findAudioAdaptation(adaptations, language, audioDescription = false) {
  const filteredAdaptations = adaptations.filter(adaptation =>
    adaptation.isAudioDescription == audioDescription
  );
  return findAdaptationByLang(filteredAdaptations, language);
}

/**
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @param {Boolean} [closedCaption=false]
 * @returns {Object|null}
 */
function findTextAdaptation(adaptations, language, closedCaption = false) {
  const filteredAdaptations = adaptations.filter(adaptation =>
    adaptation.isClosedCaption == closedCaption
  );
  return findAdaptationByLang(filteredAdaptations, language);
}

/**
 * Filter the given observable/array to only keep the item with the selected
 * type.
 * @param {Observable|Array.<Object>} stream
 * @param {string} selectedType
 * @returns {Observable|Array.<Object>}
 */
function filterByType(stream, selectedType) {
  return stream.filter(({ type }) => type === selectedType);
}

export default function(metrics, deviceEvents, options={}) {
  Object.keys(options).forEach(key =>
    options[key] === undefined && delete options[key]
  );

  const {
    defaultAudioTrack,
    defaultTextTrack,
    defaultBufferSize,
    defaultBufferThreshold,
    initialVideoBitrate,
    initialAudioBitrate,
    maxVideoBitrate,
    maxAudioBitrate,
    limitVideoWidth,
    throttleWhenHidden,
  } = Object.assign({}, DEFAULTS, options);

  const { videoWidth, inBackground } = deviceEvents;

  const $languages = new BehaviorSubject(defaultAudioTrack);
  const $subtitles = new BehaviorSubject(defaultTextTrack);

  const $averageBitrates = {
    audio: new BehaviorSubject(initialAudioBitrate),
    video: new BehaviorSubject(initialVideoBitrate),
  };

  const averageBitratesConns = [
    AverageBitrate(filterByType(metrics, "audio"), { alpha: 0.6 })
      .multicast($averageBitrates.audio),
    AverageBitrate(filterByType(metrics, "video"), { alpha: 0.6 })
      .multicast($averageBitrates.video),
  ];

  let conns = new Subscription();
  averageBitratesConns.forEach((a) => conns.add(a.connect()));

  const $usrBitrates = {
    audio: new BehaviorSubject(Infinity),
    video: new BehaviorSubject(Infinity),
  };

  const $maxBitrates = {
    audio: new BehaviorSubject(maxAudioBitrate),
    video: new BehaviorSubject(maxVideoBitrate),
  };

  const $bufSizes = {
    audio: new BehaviorSubject(defaultBufferSize),
    video: new BehaviorSubject(defaultBufferSize),
    text:  new BehaviorSubject(defaultBufferSize),
  };

  /**
   * Returns an Observable emitting:
   *   - first, the current audio adaption
   *   - the new one each time it changes
   * @param {Array.<Object>} adaptations - The available audio adaptations
   * objects.
   * @returns {Observable}
   */
  function audioAdaptationChoice(adaptations) {
    return $languages.distinctUntilChanged()
      .map(({ language, audioDescription }) =>
        findAudioAdaptation(
          adaptations,
          language,
          audioDescription
        ) || adaptations[0]
      );
  }

  /**
   * Returns an Observable emitting:
   *   - first, the current text adaption
   *   - the new one each time it changes
   * @param {Array.<Object>} adaptations - The available text adaptations
   * objects.
   * @returns {Observable}
   */
  function textAdaptationChoice(adaptations) {
    return $subtitles.distinctUntilChanged()
      .map(arg =>
        arg ? findTextAdaptation(
          adaptations,
          arg.language,
          arg.closedCaption
        ) : null
      );
  }

  /**
   * Get the current and new adaptations each time it changes for all
   * types.
   * Mostly useful for audio languages and text subtitles to know which one
   * to choose first and when it changes.
   * @param {string} type - The adaptation type
   * @param {Array.<Object>} adaptations
   * @returns {Observable}
   */
  function getAdaptationsChoice(type, adaptations) {
    if (type == "audio") {
      return audioAdaptationChoice(adaptations);
    }

    if (type == "text") {
      return textAdaptationChoice(adaptations);
    }

    return only(adaptations[0]);
  }

  /**
   * Returns an object containing two observables:
   *   - representations: the chosen best representation for the adaptation
   *     (correlated from the user, max and average bitrates)
   *   - bufferSizes: the bufferSize chosen
   * @param {Object} adaptation
   * @returns {Object}
   */
  function getBufferAdapters(adaptation) {
    const { type, representations } = adaptation;
    const bitrates = adaptation.getAvailableBitrates();

    let representationsObservable;
    if (representations.length > 1) {
      const usrBitrates = $usrBitrates[type];
      let maxBitrates = $maxBitrates[type];

      const avrBitrates = $averageBitrates[type]
        .map((avrBitrate, count) => {
          // no threshold for the first value of the average bitrate
          // stream corresponding to the selected initial video bitrate
          let bufThreshold;
          if (count === 0) {
            bufThreshold = 0;
          } else {
            bufThreshold = defaultBufferThreshold;
          }

          return getClosestBitrate(bitrates, avrBitrate, bufThreshold);
        })
        .distinctUntilChanged()
        .debounceTime(2000)
        .startWith(getClosestBitrate(bitrates, $averageBitrates[type].getValue(), 0));

      if (type == "video") {
        // To compute the bitrate upper-bound for video
        // representations we need to combine multiple stream:
        //   - user-based maximum bitrate (subject)
        //   - maximum based on the video element width
        //   - maximum based on the application visibility (background tab)
        maxBitrates = combineLatest([maxBitrates, videoWidth, inBackground])
          .map(([maxSetBitrate, width, isHidden]) => {
            if (throttleWhenHidden && isHidden) {
              return bitrates[0];
            }

            const maxUsableBitrate = limitVideoWidth ?
              getMaxUsefulBitrateforWidth(representations, width) :
              bitrates[bitrates.length - 1];

            return Math.min(maxUsableBitrate, maxSetBitrate);
          });
      }

      representationsObservable = combineLatest(
        usrBitrates,
        maxBitrates,
        avrBitrates,
        (usr, max, avr) => {
          let btr;
          if (usr < Infinity) {
            btr = usr;
          } else if (max < Infinity) {
            btr = Math.min(max, avr);
          } else {
            btr = avr;
          }
          return find(representations, (rep) => rep.bitrate === getClosestBitrate(bitrates, btr));
        })
        .distinctUntilChanged((a, b) => a.id === b.id);
    }
    else { // representations.length <= 1
      representationsObservable = only(representations[0]);
    }

    return {
      representations: representationsObservable,
      bufferSizes: $bufSizes[type] || new BehaviorSubject(defaultBufferSize),
    };
  }

  return {
    setAudioTrack(track) { $languages.next(track); },
    setTextTrack(track)  { $subtitles.next(track); },
    getAudioTrack()      { return $languages.getValue(); },
    getTextTrack()       { return $subtitles.getValue(); },

    getAverageBitrates() { return $averageBitrates; },

    getAudioMaxBitrate() { return $maxBitrates.audio.getValue(); },
    getVideoMaxBitrate() { return $maxBitrates.video.getValue(); },
    getAudioBufferSize() { return $bufSizes.audio.getValue(); },
    getVideoBufferSize() { return $bufSizes.video.getValue(); },

    setAudioBitrate(x)    { $usrBitrates.audio.next(def(x, Infinity)); },
    setVideoBitrate(x)    { $usrBitrates.video.next(def(x, Infinity)); },
    setAudioMaxBitrate(x) { $maxBitrates.audio.next(def(x, Infinity)); },
    setVideoMaxBitrate(x) { $maxBitrates.video.next(def(x, Infinity)); },
    setAudioBufferSize(x) { $bufSizes.audio.next(def(x, defaultBufferSize)); },
    setVideoBufferSize(x) { $bufSizes.video.next(def(x, defaultBufferSize)); },

    getBufferAdapters,
    getAdaptationsChoice,

    unsubscribe() {
      if (conns) {
        conns.unsubscribe();
        conns = null;
      }
    },
  };
}
<commit_msg>adaptive: update fallback bitrate when limiting by width to 0. Should never happen anyway.<commit_after>/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Subscription } from "rxjs/Subscription";
import { BehaviorSubject } from "rxjs/BehaviorSubject";
import { combineLatest } from "rxjs/observable/combineLatest";
import { only } from "../utils/rx-utils";
import { findBetterMatchIndex } from "../utils/languages";

import AverageBitrate from "./average-bitrate";

const DEFAULTS = {
  defaultAudioTrack: {
    language: "fra",
    audioDescription: false,
  },
  defaultTextTrack: null,
  // default buffer size in seconds
  defaultBufferSize: 30,
  // buffer threshold ratio used as a lower bound
  // margin to find the suitable representation
  defaultBufferThreshold: 0.3,
  initialVideoBitrate: 0,
  initialAudioBitrate: 0,
  maxVideoBitrate: Infinity,
  maxAudioBitrate: Infinity,
};

/**
 * Simple find function implementation.
 * @param {Array} array
 * @param {Function} predicate - The predicate. Will take as arguments:
 *   1. the current array element
 *   2. the array index
 *   3. the entire array
 * @returns {*} - null if not found
 */
function find(array, predicate) {
  for (let i = 0; i < array.length; i++) {
    if (predicate(array[i], i, array) === true) {
      return array[i];
    }
  }
  return null;
}

/**
 * Returns val if x is either not a Number type or inferior or equal to 0.
 * @param {Number} [x]
 * @param {*} val
 * @returns {*}
 */
function def(x, val) {
  return typeof x == "number" && x > 0 ? x : val;
}

/**
 * Get closest bitrate lower or equal to the bitrate wanted when the threshold
 * is equal to 0. You can add a security margin by setting the threshold between
 * 0 and 1.
 * @param {Array.<Number>} bitrates - all available bitrates.
 * @param {Number} btr - a chosen bitrate
 * @param {Number} [treshold=0]
 * @returns {Number}
 */
function getClosestBitrate(bitrates, btr, threshold=0) {
  for (let i = bitrates.length - 1; i >= 0; i--) {
    if ((bitrates[i] / btr) <= (1 - threshold)) {
      return bitrates[i];
    }
  }
  return bitrates[0];
}

/**
 * Get the highest bitrate from the representations having a width immediately
 * superior or equal to the given one.
 * @param {Array.<Object>} representations - The representations array
 * @param {Number} width
 * @returns {Number}
 */
function getMaxUsefulBitrateforWidth(representations, width) {
  const sortedRepsByWidth = representations.sort((a, b) => a.width - b.width);
  const firstSuperiorRepresentation =
    sortedRepsByWidth.find(r => r.width >= width);

  if (firstSuperiorRepresentation) {
    const filteredAdaptations = representations
      .filter(r => r.width <= firstSuperiorRepresentation.width);
    if (filteredAdaptations.length) {
      return filteredAdaptations[filteredAdaptations.length - 1].bitrate;
    } else {
      const firstRepresentation = representations[0];
      return (firstRepresentation && firstRepresentation.bitrate) || 0;
    }
  }

  return Infinity;
}

/**
 * Find first adaptation with the corresponding language.
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @returns {Object|null}
 */
function findAdaptationByLang(adaptations, language) {
  const languages = adaptations.map(a => a.language);

  const index = findBetterMatchIndex(languages, language);
  if (index >= 0) {
    return adaptations[index];
  }
  return null;
}

/**
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @param {Boolean} [audioDescription=false]
 * @returns {Object|null}
 */
function findAudioAdaptation(adaptations, language, audioDescription = false) {
  const filteredAdaptations = adaptations.filter(adaptation =>
    adaptation.isAudioDescription == audioDescription
  );
  return findAdaptationByLang(filteredAdaptations, language);
}

/**
 * @param {Array.<Object>} adaptations
 * @param {string} language
 * @param {Boolean} [closedCaption=false]
 * @returns {Object|null}
 */
function findTextAdaptation(adaptations, language, closedCaption = false) {
  const filteredAdaptations = adaptations.filter(adaptation =>
    adaptation.isClosedCaption == closedCaption
  );
  return findAdaptationByLang(filteredAdaptations, language);
}

/**
 * Filter the given observable/array to only keep the item with the selected
 * type.
 * @param {Observable|Array.<Object>} stream
 * @param {string} selectedType
 * @returns {Observable|Array.<Object>}
 */
function filterByType(stream, selectedType) {
  return stream.filter(({ type }) => type === selectedType);
}

export default function(metrics, deviceEvents, options={}) {
  Object.keys(options).forEach(key =>
    options[key] === undefined && delete options[key]
  );

  const {
    defaultAudioTrack,
    defaultTextTrack,
    defaultBufferSize,
    defaultBufferThreshold,
    initialVideoBitrate,
    initialAudioBitrate,
    maxVideoBitrate,
    maxAudioBitrate,
    limitVideoWidth,
    throttleWhenHidden,
  } = Object.assign({}, DEFAULTS, options);

  const { videoWidth, inBackground } = deviceEvents;

  const $languages = new BehaviorSubject(defaultAudioTrack);
  const $subtitles = new BehaviorSubject(defaultTextTrack);

  const $averageBitrates = {
    audio: new BehaviorSubject(initialAudioBitrate),
    video: new BehaviorSubject(initialVideoBitrate),
  };

  const averageBitratesConns = [
    AverageBitrate(filterByType(metrics, "audio"), { alpha: 0.6 })
      .multicast($averageBitrates.audio),
    AverageBitrate(filterByType(metrics, "video"), { alpha: 0.6 })
      .multicast($averageBitrates.video),
  ];

  let conns = new Subscription();
  averageBitratesConns.forEach((a) => conns.add(a.connect()));

  const $usrBitrates = {
    audio: new BehaviorSubject(Infinity),
    video: new BehaviorSubject(Infinity),
  };

  const $maxBitrates = {
    audio: new BehaviorSubject(maxAudioBitrate),
    video: new BehaviorSubject(maxVideoBitrate),
  };

  const $bufSizes = {
    audio: new BehaviorSubject(defaultBufferSize),
    video: new BehaviorSubject(defaultBufferSize),
    text:  new BehaviorSubject(defaultBufferSize),
  };

  /**
   * Returns an Observable emitting:
   *   - first, the current audio adaption
   *   - the new one each time it changes
   * @param {Array.<Object>} adaptations - The available audio adaptations
   * objects.
   * @returns {Observable}
   */
  function audioAdaptationChoice(adaptations) {
    return $languages.distinctUntilChanged()
      .map(({ language, audioDescription }) =>
        findAudioAdaptation(
          adaptations,
          language,
          audioDescription
        ) || adaptations[0]
      );
  }

  /**
   * Returns an Observable emitting:
   *   - first, the current text adaption
   *   - the new one each time it changes
   * @param {Array.<Object>} adaptations - The available text adaptations
   * objects.
   * @returns {Observable}
   */
  function textAdaptationChoice(adaptations) {
    return $subtitles.distinctUntilChanged()
      .map(arg =>
        arg ? findTextAdaptation(
          adaptations,
          arg.language,
          arg.closedCaption
        ) : null
      );
  }

  /**
   * Get the current and new adaptations each time it changes for all
   * types.
   * Mostly useful for audio languages and text subtitles to know which one
   * to choose first and when it changes.
   * @param {string} type - The adaptation type
   * @param {Array.<Object>} adaptations
   * @returns {Observable}
   */
  function getAdaptationsChoice(type, adaptations) {
    if (type == "audio") {
      return audioAdaptationChoice(adaptations);
    }

    if (type == "text") {
      return textAdaptationChoice(adaptations);
    }

    return only(adaptations[0]);
  }

  /**
   * Returns an object containing two observables:
   *   - representations: the chosen best representation for the adaptation
   *     (correlated from the user, max and average bitrates)
   *   - bufferSizes: the bufferSize chosen
   * @param {Object} adaptation
   * @returns {Object}
   */
  function getBufferAdapters(adaptation) {
    const { type, representations } = adaptation;
    const bitrates = adaptation.getAvailableBitrates();

    let representationsObservable;
    if (representations.length > 1) {
      const usrBitrates = $usrBitrates[type];
      let maxBitrates = $maxBitrates[type];

      const avrBitrates = $averageBitrates[type]
        .map((avrBitrate, count) => {
          // no threshold for the first value of the average bitrate
          // stream corresponding to the selected initial video bitrate
          let bufThreshold;
          if (count === 0) {
            bufThreshold = 0;
          } else {
            bufThreshold = defaultBufferThreshold;
          }

          return getClosestBitrate(bitrates, avrBitrate, bufThreshold);
        })
        .distinctUntilChanged()
        .debounceTime(2000)
        .startWith(getClosestBitrate(bitrates, $averageBitrates[type].getValue(), 0));

      if (type == "video") {
        // To compute the bitrate upper-bound for video
        // representations we need to combine multiple stream:
        //   - user-based maximum bitrate (subject)
        //   - maximum based on the video element width
        //   - maximum based on the application visibility (background tab)
        maxBitrates = combineLatest([maxBitrates, videoWidth, inBackground])
          .map(([maxSetBitrate, width, isHidden]) => {
            if (throttleWhenHidden && isHidden) {
              return bitrates[0];
            }

            const maxUsableBitrate = limitVideoWidth ?
              getMaxUsefulBitrateforWidth(representations, width) :
              bitrates[bitrates.length - 1];

            return Math.min(maxUsableBitrate, maxSetBitrate);
          });
      }

      representationsObservable = combineLatest(
        usrBitrates,
        maxBitrates,
        avrBitrates,
        (usr, max, avr) => {
          let btr;
          if (usr < Infinity) {
            btr = usr;
          } else if (max < Infinity) {
            btr = Math.min(max, avr);
          } else {
            btr = avr;
          }
          return find(representations, (rep) => rep.bitrate === getClosestBitrate(bitrates, btr));
        })
        .distinctUntilChanged((a, b) => a.id === b.id);
    }
    else { // representations.length <= 1
      representationsObservable = only(representations[0]);
    }

    return {
      representations: representationsObservable,
      bufferSizes: $bufSizes[type] || new BehaviorSubject(defaultBufferSize),
    };
  }

  return {
    setAudioTrack(track) { $languages.next(track); },
    setTextTrack(track)  { $subtitles.next(track); },
    getAudioTrack()      { return $languages.getValue(); },
    getTextTrack()       { return $subtitles.getValue(); },

    getAverageBitrates() { return $averageBitrates; },

    getAudioMaxBitrate() { return $maxBitrates.audio.getValue(); },
    getVideoMaxBitrate() { return $maxBitrates.video.getValue(); },
    getAudioBufferSize() { return $bufSizes.audio.getValue(); },
    getVideoBufferSize() { return $bufSizes.video.getValue(); },

    setAudioBitrate(x)    { $usrBitrates.audio.next(def(x, Infinity)); },
    setVideoBitrate(x)    { $usrBitrates.video.next(def(x, Infinity)); },
    setAudioMaxBitrate(x) { $maxBitrates.audio.next(def(x, Infinity)); },
    setVideoMaxBitrate(x) { $maxBitrates.video.next(def(x, Infinity)); },
    setAudioBufferSize(x) { $bufSizes.audio.next(def(x, defaultBufferSize)); },
    setVideoBufferSize(x) { $bufSizes.video.next(def(x, defaultBufferSize)); },

    getBufferAdapters,
    getAdaptationsChoice,

    unsubscribe() {
      if (conns) {
        conns.unsubscribe();
        conns = null;
      }
    },
  };
}
<|endoftext|>


<commit_before>var _ = require('lodash');
var uploadfs = require('uploadfs');
var mkdirp = require('mkdirp');
var async = require('async');

module.exports = {

  alias: 'attachments',

  afterConstruct: function(self, callback) {
    return async.series({
      enableCollection: function(callback) {
        return self.enableCollection(callback);
      },
      initUploadfs: function(callback) {
        return self.initUploadfs(callback);
      }
    }, function(err) {
      if (err) {
        return callback(err);
      }
      self.addFieldType();
      self.pushAssets();
      self.pushCreateSingleton();
      return callback(null);
    });
  },

  construct: function(self, options) {
    self.name = 'attachment';

    self.fileGroups = options.fileGroups || [
      {
        name: 'images',
        label: 'Images',
        extensions: [ 'gif', 'jpg', 'png' ],
        extensionMaps: {
          jpeg: 'jpg'
        },
        // uploadfs should treat this as an image and create scaled versions
        image: true
      },
      {
        name: 'office',
        label: 'Office',
        extensions: [ 'txt', 'rtf', 'pdf', 'xls', 'ppt', 'doc', 'pptx', 'sldx', 'ppsx', 'potx', 'xlsx', 'xltx', 'csv', 'docx', 'dotx' ],
        extensionMaps: {},
        // uploadfs should just accept this file as-is
        image: false
      },
    ];

    self.imageSizes = options.imageSizes || [
      {
        name: 'full',
        width: 1140,
        height: 1140
      },
      {
        name: 'two-thirds',
        width: 760,
        height: 760
      },
      {
        name: 'one-half',
        width: 570,
        height: 700
      },
      {
        name: 'one-third',
        width: 380,
        height: 700
      },
      // Handy for thumbnailing
      {
        name: 'one-sixth',
        width: 190,
        height: 350
      }
    ];

    var uploadfsDefaultSettings = {
      backend: 'local',
      uploadsPath: self.apos.rootDir + '/public/uploads',
      uploadsUrl: self.apos.prefix + '/uploads',
      tempPath: self.apos.rootDir + '/data/temp/uploadfs',
      // Register Apostrophe's standard image sizes. Notice you could
      // concatenate your own list of sizes if you had a need to
      imageSizes: self.imageSizes
    };

    uploadfsSettings = {};
    _.merge(uploadfsSettings, uploadfsDefaultSettings);
    _.merge(uploadfsSettings, options.uploadfs || {});

    mkdirp.sync(uploadfsSettings.uploadsPath);
    mkdirp.sync(uploadfsSettings.tempPath);

    self.uploadfs = uploadfs();

    self.initUploadfs = function(callback) {
      self.uploadfs.init(uploadfsSettings, callback);
    };

    self.enableCollection = function(callback) {
      self.apos.db.collection('aposAttachments', function(err, collection) {
        self.db = collection;
        return callback(err);
      });
    };

    require('./lib/schemaField')(self, options);
    require('./lib/api')(self, options);
    require('./lib/routes')(self, options);
    require('./lib/browser')(self, options);

  }
};
<commit_msg>factor actual work into afterConstruct<commit_after>var _ = require('lodash');
var uploadfs = require('uploadfs');
var mkdirp = require('mkdirp');
var async = require('async');

module.exports = {

  alias: 'attachments',

  afterConstruct: function(self, callback) {
    return async.series({
      enableCollection: function(callback) {
        return self.enableCollection(callback);
      },
      initUploadfs: function(callback) {
        return self.initUploadfs(callback);
      }
    }, function(err) {
      if (err) {
        return callback(err);
      }
      self.addFieldType();
      self.pushAssets();
      self.pushCreateSingleton();
      return callback(null);
    });
  },

  construct: function(self, options) {
    self.name = 'attachment';

    self.fileGroups = options.fileGroups || [
      {
        name: 'images',
        label: 'Images',
        extensions: [ 'gif', 'jpg', 'png' ],
        extensionMaps: {
          jpeg: 'jpg'
        },
        // uploadfs should treat this as an image and create scaled versions
        image: true
      },
      {
        name: 'office',
        label: 'Office',
        extensions: [ 'txt', 'rtf', 'pdf', 'xls', 'ppt', 'doc', 'pptx', 'sldx', 'ppsx', 'potx', 'xlsx', 'xltx', 'csv', 'docx', 'dotx' ],
        extensionMaps: {},
        // uploadfs should just accept this file as-is
        image: false
      },
    ];

    self.imageSizes = options.imageSizes || [
      {
        name: 'full',
        width: 1140,
        height: 1140
      },
      {
        name: 'two-thirds',
        width: 760,
        height: 760
      },
      {
        name: 'one-half',
        width: 570,
        height: 700
      },
      {
        name: 'one-third',
        width: 380,
        height: 700
      },
      // Handy for thumbnailing
      {
        name: 'one-sixth',
        width: 190,
        height: 350
      }
    ];

    var uploadfsDefaultSettings = {
      backend: 'local',
      uploadsPath: self.apos.rootDir + '/public/uploads',
      uploadsUrl: self.apos.prefix + '/uploads',
      tempPath: self.apos.rootDir + '/data/temp/uploadfs',
      // Register Apostrophe's standard image sizes. Notice you could
      // concatenate your own list of sizes if you had a need to
      imageSizes: self.imageSizes
    };

    self.uploadfsSettings = {};
    _.merge(self.uploadfsSettings, uploadfsDefaultSettings);
    _.merge(self.uploadfsSettings, options.uploadfs || {});

    self.initUploadfs = function(callback) {
      mkdirp.sync(self.uploadfsSettings.uploadsPath);
      mkdirp.sync(self.uploadfsSettings.tempPath);

      self.uploadfs = uploadfs();
      self.uploadfs.init(self.uploadfsSettings, callback);
    };

    self.enableCollection = function(callback) {
      self.apos.db.collection('aposAttachments', function(err, collection) {
        self.db = collection;
        return callback(err);
      });
    };

    require('./lib/schemaField')(self, options);
    require('./lib/api')(self, options);
    require('./lib/routes')(self, options);
    require('./lib/browser')(self, options);

  }
};
<|endoftext|>


<commit_before>"use strict";

const _ = browser.i18n.getMessage;

chrome.runtime.onInstalled.addListener(() => {
    // Open options page to initialize storage and allow
    // user to set up the connection
    browser.runtime.openOptionsPage();
});

/**
 * Gets options key from storage
 */
async function get_options () {
    return (await browser.storage.sync.get("options")).options;
}

/**
 * Gets API endpoint URL.
 */
async function get_api_url () {
    const { protocol, host, port } = await get_options();
    return `${protocol}://${host}:${port}/gui`;
}

/**
 * Gets fetch init object for valid API requests. Includes
 * HTTP auth headers and correct cookie handling.
 */
async function get_api_info () {
    const { username, password } = await get_options();

    // Base64-encoded user:pass for HTTP auth
    const credentials = window.btoa(`${username}:${password}`);

    const headers = new Headers();
    headers.append("Authorization", `Basic ${credentials}`);

    return {
        headers,               // HTTP auth headers
        credentials: 'include' // Send cookies with request
    }
}

/**
 * Fetches and parses the token.html document to get a
 * request token for API requests.
 *
 * Requires API params as it's always called from other
 * request functions that have already fetched them.
 *
 * @param api_url  API endpoint URL
 * @param api_info API fetch info
 */
async function get_token (api_url, api_info) {
    const res_body = await fetch(`${api_url}/token.html`, api_info);
    const res_text = await res_body.text();

    const parser = new DOMParser();
    const token = parser
        .parseFromString(res_text, "text/html")
        .querySelector("#token")
        .textContent;

    // Return content of token div
    return token;
}

/**
 * Makes an API request and returns the result.
 *
 * @param url_params URLSearchParams object
 */
async function do_api_request (url_params) {
    const api_url  = await get_api_url();
    const api_info = await get_api_info();

    // Append token / timestamp to request
    url_params.append("token", await get_token(api_url, api_info));
    url_params.append("t", Date.now());

    // Make request URL from params
    const url = `${api_url}/?${url_params.toString()}`;

    const res_body = await fetch(url, api_info);
    const res_json = await res_body.json();

    return res_json;
}

browser.menus.create({
    "id": "utorrent_add"
  , "title": "Add to uTorrent"
  , "contexts": [ "link" ]
    // Broken due to bug 1280370:
    // , "targetUrlPatterns": [ "magnet:*", "*://*/*.torrent" ]
});

browser.menus.onClicked.addListener(async info => {
    switch (info.menuItemId) {
        case "utorrent_add":
            // Skip if not a valid torrent URL
            if (!/(^magnet:\?|\.torrent$)/.test(info.linkUrl)) break;

            const params = new URLSearchParams();
            params.append("action", "add-url");
            params.append("s", info.linkUrl);

            try {
                await do_api_request(params);

                browser.notifications.create("success-notification", {
                    type: "basic"
                  , title: _("notification_success_title")
                  , message: _("notification_success_message")
                });
            } catch (err) {
                browser.notifications.create("failed-notification", {
                    type: "basic"
                  , title: _("notification_failed_title")
                  , message: err.toString()
                });
            }

            break;
    }
});
<commit_msg>Fix .torrent link handling with query strings<commit_after>"use strict";

const _ = browser.i18n.getMessage;

chrome.runtime.onInstalled.addListener(() => {
    // Open options page to initialize storage and allow
    // user to set up the connection
    browser.runtime.openOptionsPage();
});

/**
 * Gets options key from storage
 */
async function get_options () {
    return (await browser.storage.sync.get("options")).options;
}

/**
 * Gets API endpoint URL.
 */
async function get_api_url () {
    const { protocol, host, port } = await get_options();
    return `${protocol}://${host}:${port}/gui`;
}

/**
 * Gets fetch init object for valid API requests. Includes
 * HTTP auth headers and correct cookie handling.
 */
async function get_api_info () {
    const { username, password } = await get_options();

    // Base64-encoded user:pass for HTTP auth
    const credentials = window.btoa(`${username}:${password}`);

    const headers = new Headers();
    headers.append("Authorization", `Basic ${credentials}`);

    return {
        headers,               // HTTP auth headers
        credentials: 'include' // Send cookies with request
    }
}

/**
 * Fetches and parses the token.html document to get a
 * request token for API requests.
 *
 * Requires API params as it's always called from other
 * request functions that have already fetched them.
 *
 * @param api_url  API endpoint URL
 * @param api_info API fetch info
 */
async function get_token (api_url, api_info) {
    const res_body = await fetch(`${api_url}/token.html`, api_info);
    const res_text = await res_body.text();

    const parser = new DOMParser();
    const token = parser
        .parseFromString(res_text, "text/html")
        .querySelector("#token")
        .textContent;

    // Return content of token div
    return token;
}

/**
 * Makes an API request and returns the result.
 *
 * @param url_params URLSearchParams object
 */
async function do_api_request (url_params) {
    const api_url  = await get_api_url();
    const api_info = await get_api_info();

    // Append token / timestamp to request
    url_params.append("token", await get_token(api_url, api_info));
    url_params.append("t", Date.now());

    // Make request URL from params
    const url = `${api_url}/?${url_params.toString()}`;

    const res_body = await fetch(url, api_info);
    const res_json = await res_body.json();

    return res_json;
}

browser.menus.create({
    "id": "utorrent_add"
  , "title": "Add to uTorrent"
  , "contexts": [ "link" ]
    // Broken due to bug 1280370:
    // , "targetUrlPatterns": [ "magnet:*", "*://*/*.torrent" ]
});

browser.menus.onClicked.addListener(async info => {
    switch (info.menuItemId) {
        case "utorrent_add":
            // Skip if not a valid torrent URL
            if (!/(^magnet:\?|\.torrent(\/?(\?.*)?)?$)/.test(info.linkUrl)) break;

            const params = new URLSearchParams();
            params.append("action", "add-url");
            params.append("s", info.linkUrl);

            try {
                await do_api_request(params);

                browser.notifications.create("success-notification", {
                    type: "basic"
                  , title: _("notification_success_title")
                  , message: _("notification_success_message")
                });
            } catch (err) {
                browser.notifications.create("failed-notification", {
                    type: "basic"
                  , title: _("notification_failed_title")
                  , message: err.toString()
                });
            }

            break;
    }
});
<|endoftext|>


<commit_before>// add simple router, which handles hash URL
function route(url, callback) {
    var match = url.match(/#(.*)$/);
    fragment = match ? match[1] : url;

    console.log('route', fragment);

    history.pushState(null, null, '#' + fragment);
    callback(fragment);
}

function load_markdown(url) {
    route(url, handle_markdown);
}

// load markdown file and intercept the link
function handle_markdown(file) {
    $.get(file, function(data) {
        var content = marked(data);

        // set the content
        $('#content').html(content);
        intercept_content_link();
    });
}

function intercept_content_link() {
    // intercept all link clicks
    $('#content a').click(function(event) {
        var link = $(this).attr('href');
        if (link.endsWith('.md')) {
            event.preventDefault();
            console.log('processing', link);
            load_markdown(link, handle_markdown);
        }
    });
}

// after load
var url = window.location.href;
if (!url.endsWith('.md')) {
    url += '#index.md';
}
load_markdown(url);

window.onpopstate = function(e) {
    console.log('here');
    load_markdown(location.hash);
};

// load all files
$.get('all.txt', function(data) {
    $('#log').html(data);
});
<commit_msg>[route] simple hash url handling done.<commit_after>// add simple router, which handles hash URL
function load_url(url, callback) {
    var match = url.match(/#(.*)$/);
    fragment = match ? match[1] : url;

    history.pushState(null, null, '#' + fragment);
    callback(fragment);
}

function load_markdown(file) {
    history.pushState(null, null, '#' + file);
    handle_markdown(file)
}

window.onpopstate = function(e) {
    var hash = location.hash.slice(1);
    handle_markdown(hash);
};

// load markdown file and intercept the link
function handle_markdown(file) {
    $.get(file, function(data) {
        var content = marked(data);

        // set the content
        $('#content').html(content);
        intercept_content_link();
    });
}

// initial loading, redirect to #index.md
var url = window.location.href;
if (!url.endsWith('.md')) {
    url += '#index.md';
}
load_url(url, handle_markdown);

function intercept_content_link() {
    // intercept all link clicks
    $('#content a').click(function(event) {
        var link = $(this).attr('href');
        if (link.endsWith('.md')) {
            event.preventDefault();
            console.log('processing', link);
            load_markdown(link, handle_markdown);
        }
    });
}

// load all files
$.get('all.txt', function(data) {
    $('#log').html(data);
});
<|endoftext|>


<commit_before>$(function() {
	var conversions = {
		'uppercase': function(s) {
			return s.toUpperCase();
		},
		'lowercase': function(s) {
			return s.toLowerCase();
		},
		'URL encode': encodeURIComponent,
		'URL decode': decodeURIComponent,
		'ASCII to binary': ABC.toBinary,
		'binary to ASCII': ABC.toAscii,
		'escape HTML': function(s) {
			return s.replace(/[&<>"'\/]/g, function(c) {
				return {
					"&": "&",
					"<": "<",
					">": ">",
					'"': '"',
					"'": ''',
					"/": '/',
				}[c];
			});
		},
		'ROT-13': function(s) {
			// from http://stackoverflow.com/a/28490254/179805 by Stephen Quan
			return s.replace(/[A-Za-z]/g, function (c) {
				return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
					.charAt("NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm".indexOf(c));
			});
		},
		'reverse': function(s) {
			return s.split("").reverse().join("");
		},
	};

	$.each(conversions, function(k, v) {
		$('#conversion').append($(new Option(k, k)));
	});

	if (!Url.queryString('c')) {
		 Url.updateSearchParam('c', 'reverse');
	}
	$('#conversion').val(Url.queryString('c').replace(/_/g, ' '));
	$('#conversion').change(function() {
		Url.updateSearchParam('c', $(this).val().replace(/\s/g, '_'));
		convertIfLive();
	});

	$('#convert').click(function(e) {
		convert();
	});

	function convert() {
		var conversion = conversions[$('#conversion').val()];
		var input = $('#input').val();
		try {
			var output = conversion(input);
		}
		catch(e) {
			$('#input').addClass('error');
			return;
		}
		$('#input').removeClass('error');
		$('#output').val(output);
	}

	function convertIfLive() {
		if ($('#live').prop('checked')) {
			convert();
		}
	}

	$('#swap').click(function(e) {
		var input = $('#input').val();
		var output = $('#output').val();
		$('#input').val(output);
		$('#output').val(input);
		convertIfLive();
	});

	$('#clear').click(function(e) {
		$('#input').val('');
		$('#output').val('');
	});

	$('#input').on('input', function() {
		convertIfLive();
	});
});

// ABC - a generic, native JS (A)scii(B)inary(C)onverter.
// (c) 2013 Stephan Schmitz <eyecatchup@gmail.com>
// License: MIT, http://eyecatchup.mit-license.org
// URL: https://gist.github.com/eyecatchup/6742657
var ABC = {
	toAscii: function(bin) {
		return bin.replace(/\s*[01]{8}\s*/g, function(bin) {
			return String.fromCharCode(parseInt(bin, 2));
		});
	},
	toBinary: function(str, spaceSeparatedOctets) {
		return str.replace(/[\s\S]/g, function(str) {
			str = ABC.zeroPad(str.charCodeAt().toString(2));
			return !1 == spaceSeparatedOctets ? str : str + " ";
		});
	},
	zeroPad: function(num) {
		return "00000000".slice(String(num).length) + num;
	},
};
<commit_msg>NATO phonetic alphabet<commit_after>$(function() {
	var conversions = {
		'uppercase': function(s) {
			return s.toUpperCase();
		},
		'lowercase': function(s) {
			return s.toLowerCase();
		},
		'URL encode': encodeURIComponent,
		'URL decode': decodeURIComponent,
		'ASCII to binary': ABC.toBinary,
		'binary to ASCII': ABC.toAscii,
		'escape HTML': function(s) {
			return s.replace(/[&<>"'\/]/g, function(c) {
				return {
					"&": "&",
					"<": "<",
					">": ">",
					'"': '"',
					"'": ''',
					"/": '/',
				}[c];
			});
		},
		'ROT-13': function(s) {
			// from http://stackoverflow.com/a/28490254/179805 by Stephen Quan
			return s.replace(/[A-Za-z]/g, function (c) {
				return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
					.charAt("NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm".indexOf(c));
			});
		},
		'reverse': function(s) {
			return s.split("").reverse().join("");
		},
		'NATO phonetic alphabet': function(s) {
			var table = {
				'a': 'alpha',
				'b': 'bravo',
				'c': 'charlie',
				'd': 'delta',
				'e': 'echo',
				'f': 'foxtrot',
				'g': 'golf',
				'h': 'hotel',
				'i': 'india',
				'j': 'juliet',
				'k': 'kilo',
				'l': 'lima',
				'm': 'mike',
				'n': 'november',
				'o': 'oscar',
				'p': 'papa',
				'q': 'quebec',
				'r': 'romeo',
				's': 'sierra',
				't': 'tango',
				'u': 'uniform',
				'v': 'victor',
				'w': 'whiskey',
				'x': 'x-ray',
				'y': 'yankee',
				'z': 'zulu',
			};
			return $.map(s.split(''), function (c) {
				if (c in table) {
					return table[c];
				}
				else if (c.toLowerCase() in table) {
					return table[c.toLowerCase()].toUpperCase();
				}
				else {
					return c;
				}
			}).join(' ');
		},
	};

	$.each(conversions, function(k, v) {
		$('#conversion').append($(new Option(k, k)));
	});

	if (!Url.queryString('c')) {
		 Url.updateSearchParam('c', 'reverse');
	}
	$('#conversion').val(Url.queryString('c').replace(/_/g, ' '));
	$('#conversion').change(function() {
		Url.updateSearchParam('c', $(this).val().replace(/\s/g, '_'));
		convertIfLive();
	});

	$('#convert').click(function(e) {
		convert();
	});

	function convert() {
		var conversion = conversions[$('#conversion').val()];
		var input = $('#input').val();
		try {
			var output = conversion(input);
		}
		catch(e) {
			$('#input').addClass('error');
			return;
		}
		$('#input').removeClass('error');
		$('#output').val(output);
	}

	function convertIfLive() {
		if ($('#live').prop('checked')) {
			convert();
		}
	}

	$('#swap').click(function(e) {
		var input = $('#input').val();
		var output = $('#output').val();
		$('#input').val(output);
		$('#output').val(input);
		convertIfLive();
	});

	$('#clear').click(function(e) {
		$('#input').val('');
		$('#output').val('');
	});

	$('#input').on('input', function() {
		convertIfLive();
	});
});

// ABC - a generic, native JS (A)scii(B)inary(C)onverter.
// (c) 2013 Stephan Schmitz <eyecatchup@gmail.com>
// License: MIT, http://eyecatchup.mit-license.org
// URL: https://gist.github.com/eyecatchup/6742657
var ABC = {
	toAscii: function(bin) {
		return bin.replace(/\s*[01]{8}\s*/g, function(bin) {
			return String.fromCharCode(parseInt(bin, 2));
		});
	},
	toBinary: function(str, spaceSeparatedOctets) {
		return str.replace(/[\s\S]/g, function(str) {
			str = ABC.zeroPad(str.charCodeAt().toString(2));
			return !1 == spaceSeparatedOctets ? str : str + " ";
		});
	},
	zeroPad: function(num) {
		return "00000000".slice(String(num).length) + num;
	},
};
<|endoftext|>


<commit_before>const electron = require('electron')
// Module to control application life.
const app = electron.app
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow

const path = require('path')
const url = require('url')
const DEBUG = false;

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow

function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({width: 390, height: 530, titleBarStyle: 'hidden', resizable:false})

  // and load the index.html of the app.
  mainWindow.loadURL(url.format({
    pathname: path.join(__dirname, '/app/view/index.html'),
    protocol: 'file:',
    slashes: true
  }))

  // Open the DevTools.
  if(DEBUG)mainWindow.webContents.openDevTools()

  // Emitted when the window is closed.
  mainWindow.on('closed', function () {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', function () {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', function () {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow()
  }
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
<commit_msg>Add some menu items for Mac os<commit_after>// Module to create native browser window,
// Module to control application life,
// Module to create native application menus and context menus.
const {BrowserWindow, app, Menu} = require('electron')
const path = require('path')
const url = require('url')
const DEBUG = false;

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow

function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({width: 390, height: 530, titleBarStyle: 'hidden', resizable:false})

  // and load the index.html of the app.
  mainWindow.loadURL(url.format({
      pathname: path.join(__dirname, '/app/view/index.html'),
      protocol: 'file:',
      slashes: true
  }))

  // Open the DevTools.
  if(DEBUG) mainWindow.webContents.openDevTools()

  // Create the Application's main menu
  var template = [{
      label: "Application",
      submenu: [
          { label: "Quit", accelerator: "Command+Q", click: function() { app.quit(); }}
      ]}, {
      label: "Edit",
      submenu: [
          { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
      ]}
  ];
  Menu.setApplicationMenu(Menu.buildFromTemplate(template));

  // Emitted when the window is closed.
  mainWindow.on('closed', function () {
      // Dereference the window object, usually you would store windows
      // in an array if your app supports multi windows, this is the time
      // when you should delete the corresponding element.
      mainWindow = null
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', function () {
    // On OS X it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

app.on('activate', function () {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
      createWindow()
  }
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
<|endoftext|>


<commit_before>/*!
 * Samsaara Middleware Template
 * Copyright(c) 2015 Arjun Mehta <arjun@arjunmehta.net>
 * MIT Licensed
 */

var debug = require('debugit').add('samsaara:groups');

var Group;
var groups = {};

module.exports = {

    name: 'groups',

    initialize: function(extender, capability, options) {

        Group = require('./lib/group').initialize(extender.core, extender.execute);

        extender.addCoreMethods(this.coreMethods);
        extender.addModuleMethods(this.moduleMethods);
        extender.addConnectionInitialization(this.connectionInitialization, {
            forced: true
        });
        extender.addConnectionClose(this.connectionClose);
        extender.addMessageRoutes(this.messageRoutes);
        extender.addPreRouteFilter(this.preRouteFilter);

        createGroup('all');

        return this;
    },

    coreMethods: {
        group: group,
        createGroup: createGroup
    },

    connectionInitialization: function(connection, done) {

        debug('Initializing Groups', connection.id);

        connection.groups = {};
        groups.all.add(connection);

        done();
    },

    connectionClose: function(connection) {

        var connID = connection.id;
        var groupName;

        if (connection.groups) {

            debug('Disconnecting Client', connID, connection.groups);

            for (groupName in connection.groups) {
                group(groupName).remove(connection);
            }
        }

    }
};

function group(groupName) {
    return groups[groupName];
}

function createGroup(groupName, members) {
    var newGroup;

    members = members || {};
    newGroup = new Group(groupName, members);
    groups[groupName] = newGroup;

    return newGroup;
}
<commit_msg>Renaming coreMethods to coreObjects.<commit_after>/*!
 * Samsaara Middleware Template
 * Copyright(c) 2015 Arjun Mehta <arjun@arjunmehta.net>
 * MIT Licensed
 */

var debug = require('debugit').add('samsaara:groups');

var Group;
var groups = {};

module.exports = {

    name: 'groups',

    initialize: function(extender, capability, options) {

        Group = require('./lib/group').initialize(extender.core, extender.execute);

        extender.addCoreObjects(this.coreObjects);
        extender.addModuleMethods(this.moduleMethods);
        extender.addConnectionInitialization(this.connectionInitialization, {
            forced: true
        });
        extender.addConnectionClose(this.connectionClose);
        extender.addMessageRoutes(this.messageRoutes);
        extender.addPreRouteFilter(this.preRouteFilter);

        createGroup('all');

        return this;
    },

    coreObjects: {
        group: group,
        createGroup: createGroup
    },

    connectionInitialization: function(connection, done) {

        debug('Initializing Groups', connection.id);

        connection.groups = {};
        groups.all.add(connection);

        done();
    },

    connectionClose: function(connection) {

        var connID = connection.id;
        var groupName;

        if (connection.groups) {

            debug('Disconnecting Client', connID, connection.groups);

            for (groupName in connection.groups) {
                group(groupName).remove(connection);
            }
        }

    }
};

function group(groupName) {
    return groups[groupName];
}

function createGroup(groupName, members) {
    var newGroup;

    members = members || {};
    newGroup = new Group(groupName, members);
    groups[groupName] = newGroup;

    return newGroup;
}
<|endoftext|>


<commit_before>var express = require('express'),
    path = require('path'),
    logger = require('morgan'),
    cookieParser = require('cookie-parser'),
    bodyParser = require('body-parser'),
    ejs = require('ejs'),
    aws = require('aws-sdk');

var AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY_ID;
var AWS_SECRET_KEY = process.env.AWS_SECRET_KEY;
var S3_BUCKET = process.env.S3_BUCKET;

var app = express();
var router = express.Router();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

router.get('/', function(req, res, next) {
  res.render('pages/index', { title: 'Express' });
});

router.get('/files/uploadurl', function(req, res) {
    aws.config.update({accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY});
    var s3 = new aws.S3();
    var s3_params = {
        Bucket: S3_BUCKET,
        Key: req.query.file_name,
        Expires: 60,
        ContentType: req.query.file_type,
        ACL: 'public-read'
    };
    s3.getSignedUrl('putObject', s3_params, function(err, url){
        if(err) {
            res.write(JSON.stringify( { url: null, error: err } ));
            res.end();
        } else {
            res.write(JSON.stringify( { url: url } ));
            res.end();
        }
    });
});

router.get('/files/downloadurl', function(req, res) {
    aws.config.update({accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY});
    var s3 = new aws.S3();
    var s3_params = {
        Bucket: S3_BUCKET,
        Key: req.query.file_name,
        Expires: 60
    };
    s3.getSignedUrl('getObject', s3_params, function(err, url){
        if(err) {
            res.write(JSON.stringify( { url: null, error: err } ));
            res.end();
        } else {
            res.write(JSON.stringify( { url: url } ));
            res.end();
        }
    });
});

router.get('/tables', function(req, res) {
    aws.config.update( { accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY } );
    aws.config.update( { region: 'us-east-1' } );
    var db = new AWS.DynamoDB();
    db.listTables(function(err, data) {
        if (err) {
            res.write(JSON.stringify( { tables: null, error: err } ));
            res.end();
        } else {
            res.write(JSON.stringify( { tables: data } ));
            res.end();
        }
    });
});

router.get('/files', function(req, res) {
    aws.config.update( { accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY } );

    var s3 = new aws.S3({ params: { Bucket: S3_BUCKET } });
    s3.listObjects(function(err, data){
        if(err) {
            res.write(JSON.stringify({ files: null, error: err }));
            res.end();
        } else {
            var files = [];
            for (var i = 0; i < data.Contents.length; i++) {
                var item = data.Contents[i];
                var key = item.Key;
                var size = item.Size;
                files[i] = {
                    key: key,
                    size: size
                };
            }

            res.write(JSON.stringify( { files: files } ));
            res.end();
        }
    });
});

app.use('/', router);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
  app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('pages/error', {
      message: err.message,
      error: err
    });
  });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.render('pages/error', {
    message: err.message,
    error: {}
  });
});

module.exports = app;
<commit_msg>Fixing typo<commit_after>var express = require('express'),
    path = require('path'),
    logger = require('morgan'),
    cookieParser = require('cookie-parser'),
    bodyParser = require('body-parser'),
    ejs = require('ejs'),
    aws = require('aws-sdk');

var AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY_ID;
var AWS_SECRET_KEY = process.env.AWS_SECRET_KEY;
var S3_BUCKET = process.env.S3_BUCKET;

var app = express();
var router = express.Router();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

router.get('/', function(req, res, next) {
  res.render('pages/index', { title: 'Express' });
});

router.get('/files/uploadurl', function(req, res) {
    aws.config.update({accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY});
    var s3 = new aws.S3();
    var s3_params = {
        Bucket: S3_BUCKET,
        Key: req.query.file_name,
        Expires: 60,
        ContentType: req.query.file_type,
        ACL: 'public-read'
    };
    s3.getSignedUrl('putObject', s3_params, function(err, url){
        if(err) {
            res.write(JSON.stringify( { url: null, error: err } ));
            res.end();
        } else {
            res.write(JSON.stringify( { url: url } ));
            res.end();
        }
    });
});

router.get('/files/downloadurl', function(req, res) {
    aws.config.update({accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY});
    var s3 = new aws.S3();
    var s3_params = {
        Bucket: S3_BUCKET,
        Key: req.query.file_name,
        Expires: 60
    };
    s3.getSignedUrl('getObject', s3_params, function(err, url){
        if(err) {
            res.write(JSON.stringify( { url: null, error: err } ));
            res.end();
        } else {
            res.write(JSON.stringify( { url: url } ));
            res.end();
        }
    });
});

router.get('/tables', function(req, res) {
    aws.config.update( { accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY } );
    aws.config.update( { region: 'us-east-1' } );
    var db = new aws.DynamoDB();
    db.listTables(function(err, data) {
        if (err) {
            res.write(JSON.stringify( { tables: null, error: err } ));
            res.end();
        } else {
            res.write(JSON.stringify( { tables: data } ));
            res.end();
        }
    });
});

router.get('/files', function(req, res) {
    aws.config.update( { accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY } );

    var s3 = new aws.S3({ params: { Bucket: S3_BUCKET } });
    s3.listObjects(function(err, data){
        if(err) {
            res.write(JSON.stringify({ files: null, error: err }));
            res.end();
        } else {
            var files = [];
            for (var i = 0; i < data.Contents.length; i++) {
                var item = data.Contents[i];
                var key = item.Key;
                var size = item.Size;
                files[i] = {
                    key: key,
                    size: size
                };
            }

            res.write(JSON.stringify( { files: files } ));
            res.end();
        }
    });
});

app.use('/', router);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
  app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('pages/error', {
      message: err.message,
      error: err
    });
  });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.render('pages/error', {
    message: err.message,
    error: {}
  });
});

module.exports = app;
<|endoftext|>