commit 4e1b0b4c686a62b2e9b4f71b3ec83634283229cc Author: amarcu5 Date: Thu Mar 2 16:15:41 2017 +0000 Initial release Basic support for Netflix, Youtube, Amazon Video, Twitch, and more diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae73b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +certs/privatekey.pem + +certs/cert.pem diff --git a/README.md b/README.md new file mode 100644 index 0000000..1671247 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# PiPer +Adds Picture in Picture functionality to Safari for Youtube, Netflix, Amazon Video, Twitch, and more! + +## Installation + +Get the extension [here](https://s3.amazonaws.com/piper-extension/PiPer.safariextz), open the downloaded file, and hit trust + +## Supported sites +* [Youtube](http://www.youtube.com) +* [Netflix](http://www.netflix.com) +* [Amazon video](http://www.amazon.com/PrimeVideo) +* [Twitch](http://www.twitch.tv) +* [Metacafe](http://www.metacafe.com) +* [Openload](http://www.openload.co) +* [CollegeHumor](http://www.collegehumor.com) +* [Vevo](http://www.vevo.com) +* [Vid.me](http://www.vid.me) + +## Acknowledgements +* [Pied PĂ­Per](https://github.com/JoeKuhns/PiedPiPer.safariextension) for the original inspiration and the Netflix icon +* [Google](https://github.com/google/material-design-icons) for the Picture in Picture icon + +## Build tools +* [svgo](https://github.com/svg/svgo) (0.7.2) for compressing SVG images +* [xarjs](https://github.com/robertknight/xar-js) (0.2.0) for packaging Safari extension +* [ccjs](https://github.com/google/closure-compiler-js) (1.6.1) for compiling JavaScript +* [plistbuddy](https://github.com/amarcu5/PiPer/tree/master/build-tools/) for automated build numbering - reimplementation of utility from [Apple's Command Line Tools](https://developer.apple.com/download/) diff --git a/build-tools/ccjs b/build-tools/ccjs new file mode 100755 index 0000000..502ebfb Binary files /dev/null and b/build-tools/ccjs differ diff --git a/build-tools/plistbuddy b/build-tools/plistbuddy new file mode 100755 index 0000000..814716b Binary files /dev/null and b/build-tools/plistbuddy differ diff --git a/build-tools/svgo b/build-tools/svgo new file mode 100755 index 0000000..8c8f6ab Binary files /dev/null and b/build-tools/svgo differ diff --git a/build-tools/xarjs b/build-tools/xarjs new file mode 100755 index 0000000..eda9355 Binary files /dev/null and b/build-tools/xarjs differ diff --git a/certs/apple-intermediate.pem b/certs/apple-intermediate.pem new file mode 100644 index 0000000..35e707e --- /dev/null +++ b/certs/apple-intermediate.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEIjCCAwqgAwIBAgIIAd68xDltoBAwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UE +BhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsTHUFwcGxlIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBSb290IENBMB4XDTEz +MDIwNzIxNDg0N1oXDTIzMDIwNzIxNDg0N1owgZYxCzAJBgNVBAYTAlVTMRMwEQYD +VQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3JsZHdpZGUgRGV2ZWxv +cGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3aWRlIERldmVsb3Bl +ciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDKOFSmy1aqyCQ5SOmM7uxfuH8mkbw0U3rOfGOA +YXdkXqUHI7Y5/lAtFVZYcC1+xG7BSoU+L/DehBqhV8mvexj/avoVEkkVCBmsqtsq +Mu2WY2hSFT2Miuy/axiV4AOsAX2XBWfODoWVN2rtCbauZ81RZJ/GXNG8V25nNYB2 +NqSHgW44j9grFU57Jdhav06DwY3Sk9UacbVgnJ0zTlX5ElgMhrgWDcHld0WNUEi6 +Ky3klIXh6MSdxmilsKP8Z35wugJZS3dCkTm59c3hTO/AO0iMpuUhXf1qarunFjVg +0uat80YpyejDi+l5wGphZxWy8P3laLxiX27Pmd3vG2P+kmWrAgMBAAGjgaYwgaMw +HQYDVR0OBBYEFIgnFwmpthhgi+zruvZHWcVSVKO3MA8GA1UdEwEB/wQFMAMBAf8w +HwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wLgYDVR0fBCcwJTAjoCGg +H4YdaHR0cDovL2NybC5hcHBsZS5jb20vcm9vdC5jcmwwDgYDVR0PAQH/BAQDAgGG +MBAGCiqGSIb3Y2QGAgEEAgUAMA0GCSqGSIb3DQEBBQUAA4IBAQBPz+9Zviz1smwv +j+4ThzLoBTWobot9yWkMudkXvHcs1Gfi/ZptOllc34MBvbKuKmFysa/Nw0Uwj6OD +Dc4dR7Txk4qjdJukw5hyhzs+r0ULklS5MruQGFNrCk4QttkdUGwhgAqJTleMa1s8 +Pab93vcNIx0LSiaHP7qRkkykGRIZbVf1eliHe2iK5IaMSuviSRSqpd1VAKmuu0sw +ruGgsbwpgOYJd+W+NKIByn/c4grmO7i77LpilfMFY0GCzQ87HUyVpNur+cmV6U/k +TecmmYHpvPm0KdIBembhLoz2IYrF+Hjhga6/05Cdqa3zr/04GpZnMBxRpVzscYqC +tGwPDBUf +-----END CERTIFICATE----- diff --git a/certs/apple-root.pem b/certs/apple-root.pem new file mode 100644 index 0000000..a27252d --- /dev/null +++ b/certs/apple-root.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET +MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0 +MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw +bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx +FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+ ++FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1 +XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w +tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW +q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM +aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3 +R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE +ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93 +d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl +IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0 +YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj +b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp +Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc +NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP +y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7 +R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg +xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP +IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX +UKqK1drk/NAJBzewdXUh +-----END CERTIFICATE----- diff --git a/make.sh b/make.sh new file mode 100755 index 0000000..95ca1bf --- /dev/null +++ b/make.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +# +# Settings +# + +EXTENSION_NAME="PiPer" + +# Paths +CCJS_PATH="./build-tools/ccjs" # "ccjs" +XARJS_PATH="./build-tools/xarjs" # "xarjs" +SVGO_PATH="./build-tools/svgo" # "svgo" +PLISTBUDDY_PATH="./build-tools/plistbuddy" # "/usr/libexec/PlistBuddy" + +# Certifcates +LEAF_CERT_PATH="../certs/cert.pem" +INTERMEDIATE_CERT_PATH="../certs/apple-intermediate.pem" +ROOT_CERT_PATH="../certs/apple-root.pem" +PRIVATE_KEY_PATH="../certs/privatekey.pem" + + +# +# Build script +# + +# Set working directory to project root +cd $(cd -P -- "$(dirname -- "$0")" && pwd -P) + +# Remove output folder +rm -rf out + +# Make output folder +mkdir -p "out/${EXTENSION_NAME}.safariextension" + +# Copy items into output folder +cp -r src/* "out/${EXTENSION_NAME}.safariextension/" + +# Compress all supported images with SVGO +${SVGO_PATH} -q -f "out/${EXTENSION_NAME}.safariextension/images" + +# Use closure compiler to compress javascript +${CCJS_PATH} out/${EXTENSION_NAME}.safariextension/scripts/main.js \ + --compilation_level=ADVANCED_OPTIMIZATIONS \ + --warning_level=VERBOSE \ + --externs="out/${EXTENSION_NAME}.safariextension/scripts/externs.js" \ + --new_type_inf \ + --use_types_for_optimization \ + > out/${EXTENSION_NAME}.safariextension/scripts/main.min.js +mv out/${EXTENSION_NAME}.safariextension/scripts/main.min.js out/${EXTENSION_NAME}.safariextension/scripts/main.js +rm out/${EXTENSION_NAME}.safariextension/scripts/externs.js + +# Update version info from git +git=$(sh /etc/profile; which git) +number_of_commits=$(($("$git" rev-list HEAD --count) + 1)) +git_release_version=$("$git" describe --tags --always --abbrev=0) +git_release_version=${git_release_version%%-*}; +git_release_version=${git_release_version#*v}; +info_plist="out/${EXTENSION_NAME}.safariextension/Info.plist" +update_plist="update.plist" +${PLISTBUDDY_PATH} -c "Set :CFBundleVersion $number_of_commits" "$info_plist" +${PLISTBUDDY_PATH} -c "Set :CFBundleShortVersionString ${git_release_version}" "$info_plist" +${PLISTBUDDY_PATH} -c "Set \":Extension Updates:0:CFBundleVersion\" $number_of_commits" "$update_plist" +${PLISTBUDDY_PATH} -c "Set \":Extension Updates:0:CFBundleShortVersionString\" ${git_release_version#*v}" "$update_plist" + +# Package safari extension +cd out +if [[ ${XARJS_PATH} != /* ]]; then if ! [ command -v "${XARJS_PATH}" >/dev/null 2>&1 ]; then XARJS_PATH="../${XARJS_PATH}"; fi; fi +${XARJS_PATH} create "${EXTENSION_NAME}.safariextz" --cert "${LEAF_CERT_PATH}" --cert "${INTERMEDIATE_CERT_PATH}" --cert "${ROOT_CERT_PATH}" --private-key "${PRIVATE_KEY_PATH}" "${EXTENSION_NAME}.safariextension" + +# Remove unpacked extension +if [ -f "${PRIVATE_KEY_PATH}" ]; then rm -rf "${EXTENSION_NAME}.safariextension"; fi + +echo "Done." diff --git a/out/PiPer.safariextz b/out/PiPer.safariextz new file mode 100644 index 0000000..1fb6de5 Binary files /dev/null and b/out/PiPer.safariextz differ diff --git a/src/Icon-128.png b/src/Icon-128.png new file mode 100644 index 0000000..393dcc2 Binary files /dev/null and b/src/Icon-128.png differ diff --git a/src/Info.plist b/src/Info.plist new file mode 100644 index 0000000..d5bf828 --- /dev/null +++ b/src/Info.plist @@ -0,0 +1,74 @@ + + + + + Author + Adam Marcus + Builder Version + 12602.4.8 + CFBundleDisplayName + PiPer + CFBundleIdentifier + com.amarcus.safari.piper + CFBundleInfoDictionaryVersion + 6.0 + CFBundleShortVersionString + 0.0 + CFBundleVersion + 0 + Content + + Scripts + + End + + scripts/main.js + + + + Description + Adds Picture in Picture functionality to Youtube, Netflix, Amazon Video, Twitch, and more! + DeveloperIdentifier + BQ6Q24MF9X + ExtensionInfoDictionaryVersion + 1.0 + Update Manifest URL + https://s3.amazonaws.com/piper-extension/update.plist + Website + https://github.com/amarcu5/PiPer/ + Permissions + + Website Access + + Allowed Domains + + *.amazon.co.uk + *.amazon.com + *.amazon.fr + *.amazon.de + *.amazon.ca + *.netflix.com + netflix.com + *.youtube.com + youtube.com + *.twitch.tv + twitch.tv + *.metacafe.com + metacafe.com + *.openload.co + openload.co + *.vevo.com + vevo.com + *.collegehumor.com + collegehumor.com + vid.me + *.vid.me + + Include Secure Pages + + Level + Some + + + + diff --git a/src/images/default.svg b/src/images/default.svg new file mode 100755 index 0000000..3a34ea7 --- /dev/null +++ b/src/images/default.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/images/netflix.svg b/src/images/netflix.svg new file mode 100644 index 0000000..3c418ee --- /dev/null +++ b/src/images/netflix.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/src/scripts/externs.js b/src/scripts/externs.js new file mode 100644 index 0000000..07017fd --- /dev/null +++ b/src/scripts/externs.js @@ -0,0 +1,14 @@ +/** @const */ +var safari = {}; + +/** @const */ +safari.extension = {}; + +/** @type {string} */ +safari.extension.baseURI; + +/** @type {string} */ +HTMLVideoElement.prototype.webkitPresentationMode; + +/** @return {undefined} */ +HTMLVideoElement.prototype.webkitSetPresentationMode = function(mode) {} \ No newline at end of file diff --git a/src/scripts/main.js b/src/scripts/main.js new file mode 100644 index 0000000..6c4245f --- /dev/null +++ b/src/scripts/main.js @@ -0,0 +1,247 @@ +'use strict'; + +/** + * @typedef {{ + * buttonImage: (string | undefined), + * buttonElementType: (string|undefined), + * buttonStyle: (string|undefined), + * buttonClassName: (string|undefined), + * buttonParent: function(): ?Element, + * buttonInsertBefore: (function(Element): ?Node|undefined), + * videoElement: function(): ?Element, + * buttonWillAppear: (function(): undefined|undefined), + * }} + */ +var PIPResource; + + +/** @define {boolean} */ +const COMPILED = false; + +function log(/** string */ message) { + !COMPILED && console.log("PIPer: " + message); +} + + +const BUTTON_ID = 'PIPButton'; + +var /** boolean */ buttonAdded = false; +var /** ?PIPResource */ currentResource = null; + +const addButton = function (/** Element */ parent) { + const button = document.createElement(currentResource.buttonElementType || 'button'); + + button.id = BUTTON_ID; + button.title = 'Open Picture in Picture mode'; + if (currentResource.buttonStyle) button.style.cssText = currentResource.buttonStyle; + if (currentResource.buttonClassName) button.className = currentResource.buttonClassName; + + const image = document.createElement('img'); + image.src = safari.extension.baseURI + 'images/' + (currentResource.buttonImage || 'default') + '.svg'; + image.style.cssText = 'width:100%;height:100%'; + + button.appendChild(image); + + button.addEventListener('click', function (event) { + event.preventDefault(); + + const video = /** @type {?HTMLVideoElement} */ (currentResource.videoElement()); + if (!video) return; + + video.webkitSetPresentationMode('inline' === video.webkitPresentationMode ? 'picture-in-picture' : 'inline'); + }); + + parent.insertBefore(button, currentResource.buttonInsertBefore ? currentResource.buttonInsertBefore(parent) : null); +} + +const buttonObserver = function () { + + if (buttonAdded) { + if (document.getElementById(BUTTON_ID)) return; + log("Button removed"); + buttonAdded = false; + } + + const buttonParent = currentResource.buttonParent(); + if (buttonParent) { + if (currentResource.buttonWillAppear) currentResource.buttonWillAppear(); + addButton(buttonParent); + log("Button added"); + buttonAdded = true; + } +}; + +/** @type {!IObject} */ +const resources = { + 'amazon': { + buttonStyle: 'border:0;padding:0;margin:0;background-color:transparent;opacity:0.8;position:relative;left:-8px;width:2vw;height:2vw;min-width:20px;min-height:20px', + + buttonParent: function () { + const e = document.getElementById('dv-web-player'); + return e && e.querySelector('.hideableTopButtons'); + }, + + buttonInsertBefore: function (/** Element */ parent) { + return parent.lastChild; + }, + + videoElement: function () { + const e = document.querySelector('.rendererContainer'); + return e && e.querySelector('video[width="100%"]'); + } + }, + 'youtube': { + buttonStyle: 'transform:scale(0.7)', + + buttonClassName: 'ytp-button', + + buttonParent: function () { + const e = document.getElementById('movie_player') || document.getElementById('player'); + return e && e.querySelector('.ytp-right-controls'); + }, + + videoElement: function () { + const e = document.getElementById('movie_player') || document.getElementById('player'); + return e && e.querySelector('video.html5-main-video'); + } + }, + 'netflix': { + buttonImage: 'netflix', + + buttonElementType: 'span', + + buttonStyle: 'position:absolute;padding:4px;right:0;top:.4em;width:2em;height:2em;cursor:pointer;background-color:#262626', + + buttonParent: function () { + const e = document.getElementById('playerContainer'); + return e ? e.querySelector('.player-status') : null; + }, + + buttonWillAppear: function () { + resources['netflix'].buttonParent().style.paddingRight = '50px'; + }, + + videoElement: function () { + const e = document.querySelector('.player-video-wrapper'); + return e && e.querySelector('video'); + } + }, + 'twitch': { + buttonStyle: 'transform:scale(0.8)', + + buttonClassName: 'player-button', + + buttonParent: function () { + const e = document.getElementById('video-playback') || document.getElementById('player'); + return e && e.querySelector('.player-buttons-right'); + }, + + videoElement: function () { + const e = document.getElementById('video-playback') || document.getElementById('player'); + return e && e.querySelector('video'); + } + }, + 'metacafe': { + buttonElementType: 'div', + + buttonStyle: 'transform:scale(0.9);left:-2px', + + buttonParent: function () { + const e = document.getElementById('player_place'); + return e && e.querySelector('.tray'); + }, + + videoElement: function () { + const e = document.getElementById('player_place'); + return e && e.querySelector('video'); + } + }, + 'openload': { + buttonStyle: 'transform:scale(0.6);left:5px', + + buttonClassName: 'vjs-control vjs-button', + + buttonInsertBefore: function (/** Element */ parent) { + return parent.lastChild; + }, + + buttonParent: function () { + const e = document.getElementById('olvideo'); + return e && e.querySelector('.vjs-control-bar'); + }, + + videoElement: function () { + return document.getElementById('olvideo_html5_api'); + } + }, + 'vevo': { + buttonStyle: 'transform:scale(0.7);border:0;background:transparent', + + buttonClassName: 'player-control', + + buttonInsertBefore: function (/** Element */ parent) { + return parent.lastChild; + }, + + buttonParent: function () { + const e = document.getElementById('control-bar'); + return e && e.querySelector('.right-controls'); + }, + + videoElement: function () { + return document.getElementById('html5-player'); + } + }, + 'collegehumor': { + buttonStyle: 'transform:scale(0.6)', + + buttonClassName: 'vjs-control vjs-button', + + buttonInsertBefore: function (/** Element */ parent) { + return parent.lastChild; + }, + + buttonParent: function () { + const e = document.getElementById('vjs_video_3'); + return e && e.querySelector('.vjs-control-bar'); + }, + + videoElement: function () { + return document.getElementById('vjs_video_3_html5_api'); + } + }, + 'vid': { + buttonStyle: 'position:relative;left:9px;top:-2px;transform:scale(0.7);padding:0;margin:0', + + buttonInsertBefore: function (/** Element */ parent) { + return parent.lastChild; + }, + + buttonParent: function () { + const e = document.getElementById('video_player'); + return e && e.querySelector('.vjs-control-bar'); + }, + + videoElement: function () { + return document.getElementById('video_player_html5_api'); + } + } +}; + + +const domainName = location.hostname.match(/([^.]+)\.(?:co\.)?[^.]+$/)[1]; + +if (domainName in resources) { + log("Matched site " + domainName + " (" + location + ")"); + currentResource = resources[domainName]; + + const observer = new MutationObserver(buttonObserver); + + observer.observe(document, { + childList:true, + subtree:true + }); + + buttonObserver(); +} + diff --git a/update.plist b/update.plist new file mode 100644 index 0000000..625d1ea --- /dev/null +++ b/update.plist @@ -0,0 +1,23 @@ + + + + + Extension Updates + + + CFBundleIdentifier + com.amarcus.safari.piper + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 1 + Developer Identifier + BQ6Q24MF9X + URL + https://s3.amazonaws.com/piper-extension/PiPer.safariextz + Update From Gallery + + + + +