├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── addon.txt ├── design ├── Icons │ ├── back.png │ ├── close.png │ ├── delete.png │ ├── fav_star.png │ ├── fav_star_outline.png │ ├── forward.png │ ├── home.png │ ├── pause.png │ ├── play.png │ ├── plus.png │ ├── refresh.png │ ├── skip.png │ ├── thumbs_down.png │ ├── thumbs_up.png │ └── volume.png ├── Services │ ├── reddit.png │ ├── shoutcast.png │ ├── soundcloud.png │ ├── twitch.png │ ├── vimeo.png │ └── youtube.png └── media-queue.png ├── html ├── .gitattributes ├── .gitignore ├── .jshintrc ├── Gruntfile.js ├── app │ ├── fonts │ │ ├── Oswald.eot │ │ ├── Oswald.svg │ │ ├── Oswald.ttf │ │ └── Oswald.woff │ ├── images │ │ └── logos │ │ │ ├── blip.png │ │ │ ├── reddit.png │ │ │ ├── shoutcast.png │ │ │ ├── soundcloud.png │ │ │ ├── twitch.png │ │ │ ├── vimeo.svg │ │ │ └── youtube.png │ ├── index.html │ ├── privacy.html │ ├── redirect.html │ ├── request.html │ ├── scripts │ │ ├── main.js │ │ ├── request.js │ │ └── services │ │ │ └── twitch.js │ ├── styles │ │ ├── lib │ │ │ └── normalize.scss │ │ ├── main.scss │ │ └── request.scss │ ├── tos.html │ ├── vimeo.html │ └── youtube.html ├── bower.json ├── package-lock.json └── package.json ├── lua ├── autorun │ ├── includes │ │ ├── extensions │ │ │ └── sh_url.lua │ │ └── modules │ │ │ ├── EventEmitter.lua │ │ │ ├── browserpool.lua │ │ │ ├── htmlmaterial.lua │ │ │ ├── inputhook.lua │ │ │ └── spritesheet.lua │ ├── mediaplayer.lua │ ├── mediaplayer_spawnables.lua │ ├── menubar │ │ └── mp_options.lua │ ├── properties │ │ └── mediaplayer.lua │ └── sandbox │ │ └── mediaplayer_dupe.lua ├── entities │ ├── mediaplayer_base │ │ ├── cl_init.lua │ │ ├── init.lua │ │ └── shared.lua │ └── mediaplayer_tv │ │ └── shared.lua ├── mediaplayer │ ├── cl_idlescreen.lua │ ├── cl_init.lua │ ├── cl_requests.lua │ ├── cl_screen.lua │ ├── config │ │ ├── client.lua │ │ └── server.lua │ ├── controls │ │ ├── dhtmlcontrols.lua │ │ ├── dmediaplayerhtml.lua │ │ └── dmediaplayerrequest.lua │ ├── init.lua │ ├── players │ │ ├── base │ │ │ ├── cl_draw.lua │ │ │ ├── cl_fullscreen.lua │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ ├── net.lua │ │ │ ├── sh_snapshot.lua │ │ │ └── shared.lua │ │ ├── components │ │ │ ├── vote.lua │ │ │ └── voteskip.lua │ │ └── entity │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ ├── sh_meta.lua │ │ │ └── shared.lua │ ├── services │ │ ├── audiofile │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ └── shared.lua │ │ ├── base │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ └── shared.lua │ │ ├── browser.lua │ │ ├── googledrive │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ └── shared.lua │ │ ├── html5_video.lua │ │ ├── image.lua │ │ ├── resource │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ └── shared.lua │ │ ├── shoutcast.lua │ │ ├── soundcloud │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ └── shared.lua │ │ ├── twitch │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ └── shared.lua │ │ ├── twitchstream │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ └── shared.lua │ │ ├── vimeo │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ └── shared.lua │ │ ├── webpage.lua │ │ └── youtube │ │ │ ├── cl_init.lua │ │ │ ├── init.lua │ │ │ └── shared.lua │ ├── sh_cvars.lua │ ├── sh_events.lua │ ├── sh_history.lua │ ├── sh_mediaplayer.lua │ ├── sh_metadata.lua │ ├── sh_services.lua │ ├── shared.lua │ ├── sv_requests.lua │ └── utils.lua └── mp_menu │ ├── cl_init.lua │ ├── common.lua │ ├── horizontal_list.lua │ ├── icons.lua │ ├── init.lua │ ├── playback.lua │ ├── queue.lua │ ├── sidebar.lua │ ├── sidebar_tabs.lua │ └── volume_control.lua ├── materials ├── entities │ └── mediaplayer_tv.png ├── mediaplayer │ └── ui │ │ └── spritesheet2015-10-7.png ├── models │ └── gmod_tower │ │ ├── suitetv_large.vmt │ │ └── suitetv_large.vtf └── theater │ ├── STATIC.vmt │ └── STATIC.vtf ├── mediaplayer.fgd ├── mediaplayer.sublime-project ├── models └── gmod_tower │ ├── suitetv_large.dx80.vtx │ ├── suitetv_large.dx90.vtx │ ├── suitetv_large.mdl │ ├── suitetv_large.phy │ ├── suitetv_large.sw.vtx │ └── suitetv_large.vvd └── resource └── fonts └── ClearSans-Medium.ttf /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = crlf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.lua] 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [*.html] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general 2 | *.todo 3 | *.sublime-workspace 4 | design 5 | 6 | # old mediaplayer service code 7 | _deprecated 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2014 GMod Media Player authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Media Player 2 | ============ 3 | 4 | ![Preview](http://images.akamai.steamusercontent.com/ugc/403430334757512796/4EFCE2D358BCAF42389E36B62CB11E9849842E07/) 5 | 6 | Media Player is an addon for Garry's Mod which features several media streaming services able to be played synchronously in multiplayer. 7 | 8 | ### Installation ### 9 | 10 | Place the contents of this GitHub repository into a new addon folder within your `garrysmod/addons/` directory. For those unfamiliar with Git, press the `Download ZIP` button in the right-hand sidebar. 11 | 12 | If you'd only like to use the addon and not modify the source code, you can subscribe to the item on Steam Workshop: 13 | 14 | [![Steam Workshop](http://www.pixeltailgames.com/elevator/images/workshop_button.png)](http://steamcommunity.com/sharedfiles/filedetails/?id=546392647) 15 | -------------------------------------------------------------------------------- /addon.txt: -------------------------------------------------------------------------------- 1 | "AddonInfo" 2 | { 3 | "name" "Media Player" 4 | "version" "1.0.0" 5 | "author_name" "Samuel Maddock" 6 | "author_email" "sam@samuelmaddock.com" 7 | "author_url" "http://samuelmaddock.com" 8 | "info" "http://github.com/samuelmaddock/gm-mediaplayer" 9 | "override" "0" 10 | } 11 | -------------------------------------------------------------------------------- /design/Icons/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/back.png -------------------------------------------------------------------------------- /design/Icons/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/close.png -------------------------------------------------------------------------------- /design/Icons/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/delete.png -------------------------------------------------------------------------------- /design/Icons/fav_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/fav_star.png -------------------------------------------------------------------------------- /design/Icons/fav_star_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/fav_star_outline.png -------------------------------------------------------------------------------- /design/Icons/forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/forward.png -------------------------------------------------------------------------------- /design/Icons/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/home.png -------------------------------------------------------------------------------- /design/Icons/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/pause.png -------------------------------------------------------------------------------- /design/Icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/play.png -------------------------------------------------------------------------------- /design/Icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/plus.png -------------------------------------------------------------------------------- /design/Icons/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/refresh.png -------------------------------------------------------------------------------- /design/Icons/skip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/skip.png -------------------------------------------------------------------------------- /design/Icons/thumbs_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/thumbs_down.png -------------------------------------------------------------------------------- /design/Icons/thumbs_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/thumbs_up.png -------------------------------------------------------------------------------- /design/Icons/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Icons/volume.png -------------------------------------------------------------------------------- /design/Services/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Services/reddit.png -------------------------------------------------------------------------------- /design/Services/shoutcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Services/shoutcast.png -------------------------------------------------------------------------------- /design/Services/soundcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Services/soundcloud.png -------------------------------------------------------------------------------- /design/Services/twitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Services/twitch.png -------------------------------------------------------------------------------- /design/Services/vimeo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Services/vimeo.png -------------------------------------------------------------------------------- /design/Services/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/Services/youtube.png -------------------------------------------------------------------------------- /design/media-queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/design/media-queue.png -------------------------------------------------------------------------------- /html/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /html/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | bower_components 6 | test/bower_components 7 | -------------------------------------------------------------------------------- /html/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "jquery": true 21 | } 22 | -------------------------------------------------------------------------------- /html/app/fonts/Oswald.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/html/app/fonts/Oswald.eot -------------------------------------------------------------------------------- /html/app/fonts/Oswald.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/html/app/fonts/Oswald.ttf -------------------------------------------------------------------------------- /html/app/fonts/Oswald.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/html/app/fonts/Oswald.woff -------------------------------------------------------------------------------- /html/app/images/logos/blip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/html/app/images/logos/blip.png -------------------------------------------------------------------------------- /html/app/images/logos/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/html/app/images/logos/reddit.png -------------------------------------------------------------------------------- /html/app/images/logos/shoutcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/html/app/images/logos/shoutcast.png -------------------------------------------------------------------------------- /html/app/images/logos/soundcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/html/app/images/logos/soundcloud.png -------------------------------------------------------------------------------- /html/app/images/logos/twitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/html/app/images/logos/twitch.png -------------------------------------------------------------------------------- /html/app/images/logos/vimeo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /html/app/images/logos/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/html/app/images/logos/youtube.png -------------------------------------------------------------------------------- /html/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Garry's Mod MediaPlayer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /html/app/privacy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Media Player - Request 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 |

Privacy Policy

22 | 23 |
24 | None of your data is shared with the Media Player addon's creator
25 | or external parties.
26 | 
27 | Go back
28 |             
29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /html/app/redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 19 | LOADING 20 | 28 | 29 | -------------------------------------------------------------------------------- /html/app/request.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Media Player - Request 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 |

Share media from one of the selected services

22 | 23 |
24 | 25 |   31 | 32 |   38 | 39 | SHOUTcast 45 | 46 |   53 | 54 | Videos 60 | 61 | FullMoviesOnYouTube 67 | 68 |
69 | 70 |
71 |

Or request a supported URL

72 | Request 79 |
80 | 81 |
82 | Terms of Service  83 | Privacy Policy 84 |
85 | 86 |
87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /html/app/scripts/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.MP = (function () { 4 | 5 | var elem = document.body; 6 | 7 | return { 8 | 9 | setHtml: function (html) { 10 | elem.innerHTML = html; 11 | console.log(elem.innerHTML); 12 | } 13 | 14 | }; 15 | 16 | }()); 17 | -------------------------------------------------------------------------------- /html/app/scripts/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Called when the user either clicks the URL request button or presses 5 | * enter on it. 6 | */ 7 | function requestUrl() { 8 | var elem = document.getElementById('urlinput'), 9 | url = elem.value; 10 | 11 | if (url.length === 0) { return; } 12 | 13 | gmod.requestUrl(url); 14 | } 15 | 16 | /** 17 | * Called when the user presses a key while focused on the URL input 18 | * text box. 19 | * 20 | * @param {KeyboardEvent} event Keyboard event. 21 | */ 22 | function onUrlKeyDown(event) { 23 | var key = event.keyCode || event.which; 24 | 25 | // submit request when the enter key is pressed 26 | if (key === 13) { 27 | requestUrl(); 28 | } 29 | } 30 | 31 | /** 32 | * Called when a user hovers over a service icon. 33 | */ 34 | function hoverService() { 35 | console.log( 'PLAY: garrysmod/ui_hover.wav' ); 36 | } 37 | 38 | /** 39 | * Called when a user selects a service to navigate to. 40 | * 41 | * @param {HTMLElement} elem DOM element. 42 | */ 43 | function selectService(elem) { 44 | console.log( 'PLAY: garrysmod/ui_click.wav' ); 45 | 46 | var href = elem.dataset.href, 47 | overlay = (elem.dataset.overlay !== undefined); 48 | 49 | if (overlay) { 50 | gmod.openUrl(href); 51 | } else { 52 | window.location.href = href; 53 | } 54 | } 55 | 56 | (function(gmod) { 57 | if (gmod === undefined) { return; } 58 | 59 | window.setServices = function (serviceIds) { 60 | serviceIds = serviceIds.split(','); 61 | 62 | var elem, sid; 63 | var serviceElems = document.querySelectorAll('.media-service'); 64 | 65 | for (var i = 0; i < serviceElems.length; i++) { 66 | elem = serviceElems[i]; 67 | sid = elem.dataset.service; 68 | if (!sid) { continue; } 69 | 70 | sid = sid.split(' '); 71 | 72 | // hide all service icons which aren't supported 73 | for (var j = 0; j < sid.length; j++) { 74 | if (serviceIds.indexOf(sid[j]) === -1) { 75 | elem.style.display = 'none'; 76 | } 77 | } 78 | } 79 | }; 80 | 81 | gmod.getServices(); 82 | }(window.gmod)); 83 | -------------------------------------------------------------------------------- /html/app/scripts/services/twitch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | MediaPlayer = window.MediaPlayer || { 4 | init: function (player) { 5 | this.player = player; 6 | this.ready = true; 7 | }, 8 | 9 | play: function () { 10 | if (!this.ready) { return; } 11 | this.player.playVideo(); 12 | }, 13 | 14 | pause: function () { 15 | if (!this.ready) { return; } 16 | if (this.player.isPaused()) { return; } 17 | this.player.pauseVideo(); 18 | }, 19 | }; 20 | 21 | Twitch.player.ready(MediaPlayer.init.bind(MediaPlayer)); 22 | -------------------------------------------------------------------------------- /html/app/styles/main.scss: -------------------------------------------------------------------------------- 1 | // bower:scss 2 | // endbower 3 | 4 | html, body { 5 | margin: 0; 6 | padding: 0; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | * { box-sizing: border-box } 12 | 13 | body { 14 | background-color: #ececec; 15 | color: #313131; 16 | overflow: hidden; 17 | } 18 | -------------------------------------------------------------------------------- /html/app/styles/request.scss: -------------------------------------------------------------------------------- 1 | @import "lib/normalize"; 2 | 3 | @mixin clearfix { 4 | &:after { 5 | content: ""; 6 | display: table; 7 | clear: both; 8 | } 9 | } 10 | 11 | html, body { 12 | width: 100%; 13 | height: 100%; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | * { 19 | -webkit-box-sizing: border-box; 20 | box-sizing: border-box; 21 | 22 | &::selection { 23 | background: rgba(255,255,255,0.11); 24 | } 25 | } 26 | 27 | body { 28 | background: -webkit-radial-gradient(center, ellipse cover, #242F3A 0%,#161616 100%); 29 | color: white; 30 | font-family: "Oswald", sans-serif; 31 | } 32 | 33 | .content-container { 34 | width: 100%; 35 | height: 100%; 36 | display: -webkit-box; 37 | -webkit-box-orient: vertical; 38 | -webkit-box-align: center; 39 | -webkit-box-pack: center; 40 | overflow: auto; 41 | } 42 | 43 | h1 { 44 | font-size: 20pt; 45 | text-shadow: 0px 0px 8px rgba(0,0,0,0.88); 46 | } 47 | 48 | .services { 49 | text-align: center; 50 | font-size: 0; 51 | max-width: 540px; 52 | 53 | @include clearfix; 54 | 55 | > .media-service { 56 | width: 120px; 57 | height: 120px; 58 | margin: 30px; 59 | 60 | display: inline-block; 61 | 62 | background-color: #EEE; 63 | background-repeat: no-repeat; 64 | background-position: center center; 65 | border-radius: 8pt; 66 | 67 | font-size: 8pt; 68 | line-height: 194px; 69 | 70 | cursor: pointer; 71 | color: transparent; 72 | 73 | &:hover { 74 | box-shadow: 0px 10px 33px rgba(0,0,0,0.66); 75 | } 76 | 77 | &.subtext { 78 | color: #666; 79 | } 80 | } 81 | } 82 | 83 | // Logos 84 | .logo-reddit { background-image: url(../images/logos/reddit.png); } 85 | .logo-shoutcast { background-image: url(../images/logos/shoutcast.png); } 86 | .logo-soundcloud { background-image: url(../images/logos/soundcloud.png); } 87 | .logo-twitch { background-image: url(../images/logos/twitch.png); } 88 | .logo-youtube { background-image: url(../images/logos/youtube.png); } 89 | 90 | .notice { 91 | text-align: center; 92 | color: rgba(255,255,255,0.44); 93 | line-height: 16pt; 94 | } 95 | 96 | .notice a, .notice a:visited, .notice a:hover { 97 | color: rgba(0,139,253,0.88); 98 | } 99 | 100 | .notice a:hover { 101 | text-decoration: none; 102 | } 103 | 104 | .url-notice { 105 | margin: 0 0 20px 0; 106 | } 107 | 108 | .url-container { 109 | width: 480px; 110 | text-align: center; 111 | margin-bottom: 10px; 112 | } 113 | 114 | .url-input { 115 | color: #000; 116 | 117 | left: 0; 118 | padding: 5px; 119 | height: 30px; 120 | width: 400px; 121 | float: left; 122 | z-index: 2; 123 | 124 | border: none; 125 | border-radius: 2px 0 0 2px; 126 | } 127 | 128 | .url-submit { 129 | display: inline-block; 130 | float: left; 131 | width: 80px; 132 | height: 30px; 133 | 134 | padding: 5px; 135 | background: #e74c3c; 136 | color: white; 137 | 138 | line-height: 18px; 139 | text-decoration: none; 140 | text-align: center; 141 | font-size: 12px; 142 | 143 | border: none; 144 | border-radius: 0 2px 2px 0; 145 | } 146 | 147 | .url-submit:hover { 148 | background: #c0392b; 149 | } 150 | 151 | .url-submit:active { 152 | background: #e74c3c; 153 | } 154 | 155 | @media all and (max-width: 730px) { 156 | .media-service { 157 | margin: 10px; 158 | } 159 | } 160 | 161 | @media all and (max-width: 570px) { 162 | h1 { 163 | font-size: 14pt; 164 | } 165 | 166 | .media-service { 167 | margin: 2px; 168 | width: 110px; 169 | height: 110px; 170 | } 171 | } 172 | 173 | .metastream { 174 | display: block; 175 | max-width: 600px; 176 | font-size: 18pt; 177 | font-weight: bold; 178 | margin: 10px 20px 0 20px; 179 | padding: 16px 24px; 180 | text-align: center; 181 | text-decoration: none; 182 | color: #fff; 183 | line-height: 28pt; 184 | letter-spacing: 0.5px; 185 | text-shadow: 1px 1px 1px rgba(0,0,0,0.2); 186 | border-radius: 4px; 187 | background: -webkit-linear-gradient(-60deg, #6f749e 0%, #9a8daf 31%, #d0a8b9 58%, #f8bbb1 100%); 188 | } 189 | 190 | .metastream-link { 191 | color: #2c84e2; 192 | text-decoration: underline; 193 | text-shadow: none; 194 | } 195 | 196 | a[href] { 197 | color: lightblue; 198 | } 199 | -------------------------------------------------------------------------------- /html/app/tos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Media Player - Request 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 |

Terms of Service

22 | 23 |
24 | Terms of Service
25 | Dated: August 9, 2020
26 | 
27 | You must agree to the Media Player addon's privacy policy.
28 |             
29 | 30 | 31 | 32 |
33 |
34 | 35 | 36 | 37 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /html/app/vimeo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Garry's Mod MediaPlayer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /html/app/youtube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 22 |
23 | 24 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /html/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gm-mediaplayer", 3 | "private": true, 4 | "dependencies": {}, 5 | "devDependencies": {} 6 | } 7 | -------------------------------------------------------------------------------- /html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gm-mediaplayer", 3 | "version": "0.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "http://github.com/pixeltailgames/gm-mediaplayer.git" 7 | }, 8 | "dependencies": {}, 9 | "devDependencies": { 10 | "grunt": "~0.4.1", 11 | "grunt-autoprefixer": "~0.7.2", 12 | "grunt-bower-install": "~1.4.0", 13 | "grunt-build-control": "^0.1.3", 14 | "grunt-concurrent": "~0.5.0", 15 | "grunt-contrib-clean": "~0.5.0", 16 | "grunt-contrib-concat": "~0.3.0", 17 | "grunt-contrib-connect": "~0.7.1", 18 | "grunt-contrib-copy": "~0.5.0", 19 | "grunt-contrib-cssmin": "~0.9.0", 20 | "grunt-contrib-htmlmin": "~0.2.0", 21 | "grunt-contrib-jshint": "~0.9.2", 22 | "grunt-contrib-uglify": "~0.4.0", 23 | "grunt-contrib-watch": "~0.6.1", 24 | "grunt-newer": "~0.7.0", 25 | "grunt-rev": "~0.1.0", 26 | "grunt-sass": "^3.1.0", 27 | "grunt-usemin": "~2.1.0", 28 | "jshint-stylish": "~0.1.5", 29 | "load-grunt-tasks": "~0.4.0", 30 | "node-sass": "^4.14.1", 31 | "time-grunt": "~0.3.1" 32 | }, 33 | "engines": { 34 | "node": ">=0.10.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lua/autorun/includes/modules/EventEmitter.lua: -------------------------------------------------------------------------------- 1 | --- 2 | -- EventEmitter 3 | -- 4 | -- Based off of Wolfy87's JavaScript EventEmitter 5 | -- 6 | local EventEmitter = {} 7 | 8 | local function indexOfListener(listeners, listener) 9 | local value 10 | local i = #listeners 11 | 12 | 13 | while i > 0 do 14 | value = listeners[i] 15 | if type(value) == 'table' and value.listener == listener then 16 | return i 17 | end 18 | i = i - 1 19 | end 20 | 21 | return -1 22 | end 23 | 24 | function EventEmitter:new(obj) 25 | if obj then 26 | table.Inherit(obj, self) 27 | else 28 | return setmetatable({}, self) 29 | end 30 | end 31 | 32 | function EventEmitter:getListeners(evt) 33 | local events = self:_getEvents() 34 | local response 35 | 36 | -- TODO: accept pattern matching 37 | 38 | if not events[evt] then 39 | local tbl = {} 40 | tbl.__array = true 41 | events[evt] = tbl 42 | end 43 | 44 | response = events[evt] 45 | 46 | return response 47 | end 48 | 49 | --[[function EventEmitter:flattenListeners(listeners) 50 | 51 | end]] 52 | 53 | function EventEmitter:getListenersAsObject(evt) 54 | local listeners = self:getListeners(evt) 55 | local response 56 | 57 | if listeners.__array then 58 | response = {} 59 | response[evt] = listeners 60 | end 61 | 62 | return response or listeners, wrapped 63 | end 64 | 65 | function EventEmitter:addListener(evt, listener) 66 | local listeners = self:getListenersAsObject(evt) 67 | local listenerIsWrapped = type(listener) == 'table' 68 | 69 | for key, _ in pairs(listeners) do 70 | if rawget(listeners, key) and indexOfListener(listeners[key], listener) == -1 then 71 | local value 72 | 73 | if listenerIsWrapped then 74 | value = listener 75 | else 76 | value = { 77 | listener = listener, 78 | once = false 79 | } 80 | end 81 | 82 | table.insert(listeners[key], value) 83 | end 84 | end 85 | 86 | return self 87 | end 88 | 89 | EventEmitter.on = EventEmitter.addListener 90 | 91 | function EventEmitter:addOnceListener(evt, listener) 92 | return self:addListener(evt, { 93 | listener = listener, 94 | once = true 95 | }) 96 | end 97 | 98 | EventEmitter.once = EventEmitter.addOnceListener 99 | 100 | function EventEmitter:removeListener(evt, listener) 101 | local listeners = self:getListenersAsObject(evt) 102 | local index 103 | 104 | for key, _ in pairs(listeners) do 105 | if rawget(listeners, key) then 106 | index = indexOfListener(listeners[key], listener) 107 | 108 | if index ~= -1 then 109 | table.remove(listeners[key], index) 110 | end 111 | end 112 | end 113 | 114 | return self 115 | end 116 | 117 | EventEmitter.off = EventEmitter.removeListener 118 | 119 | --[[function EventEmitter:addListeners(evt, listeners) 120 | 121 | end]] 122 | 123 | function EventEmitter:removeEvent(evt) 124 | local typeStr = type(evt) 125 | local events = self:_getEvents() 126 | local key 127 | 128 | if typeStr == 'string' then 129 | events[evt] = nil 130 | else 131 | self._events = nil 132 | end 133 | 134 | return self 135 | end 136 | 137 | EventEmitter.removeAllListeners = EventEmitter.removeEvent 138 | 139 | function EventEmitter:emitEvent(evt, ...) 140 | local listeners = self:getListenersAsObject(evt) 141 | local listener, i, key, response 142 | 143 | for key, _ in pairs(listeners) do 144 | if rawget(listeners, key) then 145 | i = #listeners[key] 146 | 147 | while i > 0 do 148 | listener = listeners[key][i] 149 | 150 | if listener.once == true then 151 | self:removeListener(evt, listener.listener) 152 | end 153 | 154 | response = listener.listener(...) 155 | 156 | if response == self:_getOnceReturnValue() then 157 | self:removeListener(evt, listener.listener) 158 | end 159 | 160 | i = i - 1 161 | end 162 | end 163 | end 164 | 165 | return self 166 | end 167 | 168 | EventEmitter.trigger = EventEmitter.emitEvent 169 | EventEmitter.emit = EventEmitter.emitEvent 170 | 171 | function EventEmitter:setOnceReturnValue(value) 172 | self._onceReturnValue = value 173 | return self 174 | end 175 | 176 | function EventEmitter:_getOnceReturnValue() 177 | if rawget(self, '_onceReturnValue') then 178 | return self._onceReturnValue 179 | else 180 | return true 181 | end 182 | end 183 | 184 | function EventEmitter:_getEvents() 185 | if not self._events then 186 | self._events = {} 187 | end 188 | 189 | return self._events 190 | end 191 | 192 | _G.EventEmitter = EventEmitter 193 | -------------------------------------------------------------------------------- /lua/autorun/includes/modules/inputhook.lua: -------------------------------------------------------------------------------- 1 | local IsValid = IsValid 2 | local pairs = pairs 3 | local RealTime = RealTime 4 | local type = type 5 | local IsKeyDown = input.IsKeyDown 6 | local IsMouseDown = input.IsMouseDown 7 | local IsGameUIVisible = gui.IsGameUIVisible 8 | local IsConsoleVisible = gui.IsConsoleVisible 9 | 10 | _G.inputhook = {} 11 | 12 | local HoldTime = 0.3 13 | 14 | local LastPress = nil 15 | local LastKey = nil 16 | local KeyControls = {} 17 | 18 | local function getEventArgs( a, b, c ) 19 | if c == nil then 20 | return a, b 21 | else 22 | return b, c 23 | end 24 | end 25 | 26 | local function InputThink() 27 | 28 | if IsGameUIVisible() or IsConsoleVisible() then return end 29 | 30 | local dispatch, down, held, downFunc 31 | 32 | for key, handles in pairs( KeyControls ) do 33 | for name, tbl in pairs( handles ) do 34 | 35 | dispatch = false 36 | downFunc = tbl.Mouse and IsMouseDown or IsKeyDown 37 | 38 | if tbl.Enabled then 39 | 40 | -- Key hold (repeat press) 41 | if tbl.LastPress and tbl.LastPress + HoldTime < RealTime() then 42 | dispatch = true 43 | down = true 44 | held = true 45 | 46 | tbl.LastPress = RealTime() 47 | end 48 | 49 | -- Key release 50 | if not downFunc( key ) then 51 | dispatch = true 52 | down = false 53 | 54 | tbl.Enabled = false 55 | end 56 | 57 | else 58 | 59 | -- Key press 60 | if downFunc( key ) then 61 | dispatch = true 62 | down = true 63 | 64 | tbl.Enabled = true 65 | tbl.LastPress = RealTime() 66 | end 67 | 68 | end 69 | 70 | if dispatch then 71 | -- Use same behavior as the hook system 72 | if type(name) == 'table' then 73 | if IsValid(name) then 74 | tbl.Toggle( name, down, held, key ) 75 | else 76 | handles[ name ] = nil 77 | end 78 | else 79 | tbl.Toggle( down, held, key ) 80 | end 81 | end 82 | 83 | end 84 | end 85 | 86 | end 87 | hook.Add( "Think", "InputManagerThink", InputThink ) 88 | 89 | --- 90 | -- Adds a callback to be dispatched when a key is pressed. 91 | -- 92 | -- @param key `KEY_` enum. 93 | -- @param name Unique identifier or a valid object. 94 | -- @param onToggle Callback function. 95 | -- 96 | function inputhook.Add( key, name, onToggle, isMouse ) 97 | 98 | if not (key and onToggle) then return end 99 | 100 | if not KeyControls[ key ] then 101 | KeyControls[ key ] = {} 102 | end 103 | 104 | KeyControls[ key ][ name ] = { 105 | Enabled = false, 106 | LastPress = 0, 107 | Toggle = onToggle, 108 | Mouse = isMouse 109 | } 110 | 111 | end 112 | 113 | function inputhook.AddKeyPress( key, name, onToggle ) 114 | 115 | inputhook.Add( key, name, function( a, b, c ) 116 | local down, held = getEventArgs(a, b, c) 117 | 118 | -- ignore if key down, but held OR key is not down 119 | if down then 120 | if held then return end 121 | else 122 | return 123 | end 124 | 125 | onToggle( a, b, c ) 126 | end ) 127 | 128 | end 129 | 130 | function inputhook.AddKeyRelease( key, name, onToggle ) 131 | 132 | inputhook.Add( key, name, function( a, b, c ) 133 | local down, held = getEventArgs(a, b, c) 134 | 135 | -- ignore if key is down 136 | if down then return end 137 | 138 | onToggle( a, b, c ) 139 | end ) 140 | 141 | end 142 | 143 | --- 144 | -- Removes a registered key callback. 145 | -- 146 | -- @param key `KEY_` enum. 147 | -- @param name Unique identifier or a valid object. 148 | -- 149 | function inputhook.Remove( key, name ) 150 | 151 | if not KeyControls[ key ] then return end 152 | 153 | KeyControls[ key ][ name ] = nil 154 | 155 | end 156 | -------------------------------------------------------------------------------- /lua/autorun/includes/modules/spritesheet.lua: -------------------------------------------------------------------------------- 1 | local math = math 2 | local surface = surface 3 | local table = table 4 | 5 | _G.spritesheet = {} 6 | 7 | local icons = {} 8 | 9 | --[[ 10 | Icon format example: 11 | { 12 | name = "example-icon", -- icon name 13 | mat = Material( "path/spritesheet.png" ), -- material for spritesheet 14 | w = 32, -- icon width 15 | h = 32, -- icon height 16 | xoffset = 64, -- x-axis offset relative to the texture (optional) 17 | yoffset = 128 -- y-axis offset relative to the texture (optional) 18 | } 19 | ]] 20 | 21 | local function registerIcon( icon ) 22 | local name = icon.name 23 | if not name then 24 | MsgN( "Icon has no name" ) 25 | return false 26 | end 27 | 28 | local mat = icon.mat 29 | if not mat or mat:IsError() then 30 | MsgN( "Icon '" .. name .. "' uses an invalid material '" .. mat:GetName() .. "'" ) 31 | return false 32 | end 33 | 34 | -- calculate texture UV min/max coordinates 35 | local mw, mh = mat:Width(), mat:Height() 36 | local xoffset, yoffset = icon.xoffset or 0, icon.yoffset or 0 37 | local umin, vmin = xoffset / mw, yoffset / mh 38 | local umax, vmax = umin + (icon.w / mw), vmin + (icon.h / mh) 39 | 40 | icon.umin = umin 41 | icon.umax = umax 42 | icon.vmin = vmin 43 | icon.vmax = vmax 44 | 45 | -- remove unneeded properties 46 | icon.xoffset = nil 47 | icon.yoffset = nil 48 | 49 | return true 50 | end 51 | 52 | --- 53 | -- Registers a single or list of icons. 54 | -- 55 | function spritesheet.Register( iconTbl ) 56 | iconTbl = table.Copy( iconTbl or {} ) 57 | 58 | -- passed in single icon; wrap inside table for iteration 59 | if #iconTbl == 0 then 60 | iconTbl = { iconTbl } 61 | end 62 | 63 | -- register all icons 64 | for _, icon in ipairs(iconTbl) do 65 | local valid = registerIcon( icon ) 66 | if valid then 67 | icons[icon.name] = icon 68 | end 69 | end 70 | 71 | return true 72 | end 73 | 74 | --- 75 | -- Gets the icon's width and height 76 | -- 77 | function spritesheet.GetIconSize( name ) 78 | local icon = icons[name] 79 | if not icon then 80 | MsgN( "Invalid icon '" .. tostring(name) .. "' passed into spritesheet.GetIconSize!" ) 81 | return 82 | end 83 | 84 | return icon.w, icon.h 85 | end 86 | 87 | function spritesheet.DrawIcon( name, x, y, w, h, color ) 88 | local icon = icons[name] 89 | if not icon then 90 | MsgN( "Invalid icon '" .. tostring(name) .. "' passed into spritesheet.DrawIcon!" ) 91 | return 92 | end 93 | 94 | if color then surface.SetDrawColor(color) end 95 | surface.SetMaterial(icon.mat) 96 | surface.DrawTexturedRectUV( x, y, w, h, 97 | icon.umin, icon.vmin, icon.umax, icon.vmax ) 98 | end 99 | -------------------------------------------------------------------------------- /lua/autorun/mediaplayer.lua: -------------------------------------------------------------------------------- 1 | local basepath = "mediaplayer/" 2 | 3 | local function IncludeMP( filepath ) 4 | include( basepath .. filepath ) 5 | end 6 | 7 | local function PreLoadMediaPlayer() 8 | -- Check if MediaPlayer has already been loaded 9 | if MediaPlayer then 10 | MediaPlayer.__refresh = true 11 | 12 | -- HACK: Lua refresh fix; access local variable of baseclass lib 13 | local _, BaseClassTable = debug.getupvalue(baseclass.Get, 1) 14 | for classname, _ in pairs(BaseClassTable) do 15 | if classname:find("mp_") then 16 | BaseClassTable[classname] = nil 17 | end 18 | end 19 | end 20 | end 21 | 22 | local function PostLoadMediaPlayer() 23 | if SERVER then 24 | -- Reinstall media players on Lua refresh 25 | for _, mp in pairs(MediaPlayer.GetAll()) do 26 | if mp:GetType() == "entity" and IsValid(mp) then 27 | local ent = mp:GetEntity() 28 | local snapshot = mp:GetSnapshot() 29 | local listeners = table.Copy(mp:GetListeners()) 30 | 31 | -- remove media player 32 | mp:Remove() 33 | 34 | -- install new media player 35 | ent:InstallMediaPlayer() 36 | 37 | -- restore settings 38 | mp = ent._mp 39 | mp:RestoreSnapshot( snapshot ) 40 | mp:SetListeners( listeners ) 41 | end 42 | end 43 | end 44 | end 45 | 46 | local function LoadMediaPlayer() 47 | print( "Loading 'mediaplayer' addon..." ) 48 | 49 | PreLoadMediaPlayer() 50 | 51 | -- shared includes 52 | IncludeCS "includes/extensions/sh_url.lua" 53 | IncludeCS "includes/modules/EventEmitter.lua" 54 | 55 | if SERVER then 56 | -- Add mediaplayer models 57 | resource.AddWorkshop( "546392647" ) 58 | 59 | -- download clientside includes 60 | AddCSLuaFile "includes/modules/browserpool.lua" 61 | AddCSLuaFile "includes/modules/inputhook.lua" 62 | AddCSLuaFile "includes/modules/htmlmaterial.lua" 63 | AddCSLuaFile "includes/modules/spritesheet.lua" 64 | 65 | -- initialize serverside mediaplayer 66 | IncludeMP "init.lua" 67 | else 68 | -- clientside includes 69 | include "includes/modules/browserpool.lua" 70 | include "includes/modules/inputhook.lua" 71 | include "includes/modules/htmlmaterial.lua" 72 | include "includes/modules/spritesheet.lua" 73 | 74 | -- initialize clientside mediaplayer 75 | IncludeMP "cl_init.lua" 76 | end 77 | 78 | -- Sandbox includes; these must always be included as the gamemode is still 79 | -- set as 'base' when the addon is loading. Can't check if gamemode derives 80 | -- Sandbox. 81 | if SERVER then 82 | AddCSLuaFile "menubar/mp_options.lua" 83 | AddCSLuaFile "properties/mediaplayer.lua" 84 | AddCSLuaFile "sandbox/mediaplayer_dupe.lua" 85 | else 86 | include "menubar/mp_options.lua" 87 | include "properties/mediaplayer.lua" 88 | include "sandbox/mediaplayer_dupe.lua" 89 | end 90 | 91 | -- 92 | -- Media Player menu includes; remove these if you would rather not include 93 | -- the sidebar menu. 94 | -- 95 | if SERVER then 96 | AddCSLuaFile "mp_menu/cl_init.lua" 97 | include "mp_menu/init.lua" 98 | else 99 | include "mp_menu/cl_init.lua" 100 | end 101 | 102 | PostLoadMediaPlayer() 103 | end 104 | 105 | -- First time load 106 | LoadMediaPlayer() 107 | -------------------------------------------------------------------------------- /lua/autorun/mediaplayer_spawnables.lua: -------------------------------------------------------------------------------- 1 | local MediaPlayerClass = "mediaplayer_tv" 2 | 3 | local function AddMediaPlayerModel( spawnName, name, model, playerConfig ) 4 | list.Set( "SpawnableEntities", spawnName, { 5 | PrintName = name, 6 | ClassName = MediaPlayerClass, 7 | Category = "Media Player", 8 | DropToFloor = true, 9 | KeyValues = { 10 | model = model 11 | } 12 | } ) 13 | 14 | list.Set( "MediaPlayerModelConfigs", model, playerConfig ) 15 | end 16 | 17 | AddMediaPlayerModel( 18 | "../spawnicons/models/hunter/plates/plate5x8", 19 | "Huge Billboard", 20 | "models/hunter/plates/plate5x8.mdl", 21 | { 22 | angle = Angle(0, 90, 0), 23 | offset = Vector(-118.8, 189.8, 2.5), 24 | width = 380, 25 | height = 238 26 | } 27 | ) 28 | 29 | AddMediaPlayerModel( 30 | "../spawnicons/models/props_phx/rt_screen", 31 | "Small TV", 32 | "models/props_phx/rt_screen.mdl", 33 | { 34 | angle = Angle(-90, 90, 0), 35 | offset = Vector(6.5, 27.9, 35.3), 36 | width = 56, 37 | height = 33 38 | } 39 | ) 40 | 41 | if SERVER then 42 | 43 | -- fix for media player owner not getting set on alternate model spawn 44 | hook.Add( "PlayerSpawnedSENT", "MediaPlayer.SetOwner", function(ply, ent) 45 | if not ent.IsMediaPlayerEntity then return end 46 | ent:SetCreator(ply) 47 | local mp = ent:GetMediaPlayer() 48 | mp:SetOwner(ply) 49 | end ) 50 | 51 | end 52 | -------------------------------------------------------------------------------- /lua/autorun/menubar/mp_options.lua: -------------------------------------------------------------------------------- 1 | hook.Add( "PopulateMenuBar", "MediaPlayerOptions_MenuBar", function( menubar ) 2 | 3 | local m = menubar:AddOrGetMenu( "▶ Media Player" ) 4 | 5 | m:AddCVar( "Fullscreen", "mediaplayer_fullscreen", "1", "0" ) 6 | 7 | m:AddSpacer() 8 | 9 | m:AddOption( "Turn Off All", function() 10 | for _, mp in ipairs(MediaPlayer.GetAll()) do 11 | MediaPlayer.RequestListen( mp ) 12 | end 13 | 14 | MediaPlayer.HideSidebar() 15 | end ) 16 | 17 | end ) 18 | -------------------------------------------------------------------------------- /lua/autorun/properties/mediaplayer.lua: -------------------------------------------------------------------------------- 1 | local mporder = 3200 2 | 3 | -- 4 | -- Adds a media player property. 5 | -- 6 | -- Blue icons correspond to admin actions. 7 | -- 8 | local function AddMediaPlayerProperty( name, config ) 9 | -- Assign incrementing order ID 10 | config.Order = mporder 11 | mporder = mporder + 1 12 | 13 | properties.Add( name, config ) 14 | end 15 | 16 | local function IsMediaPlayer( self, ent, ply ) 17 | return IsValid(ent) and IsValid(ply) and 18 | IsValid(ent:GetMediaPlayer()) and 19 | gamemode.Call( "CanProperty", ply, self.InternalName, ent ) 20 | end 21 | 22 | local function IsPrivilegedMediaPlayer( self, ent, ply ) 23 | return IsMediaPlayer( self, ent, ply ) and 24 | ( ply:IsAdmin() or ent:GetOwner() == ply ) 25 | end 26 | 27 | local function HasMedia( mp ) 28 | return mp:GetPlayerState() >= MP_STATE_PLAYING 29 | end 30 | 31 | AddMediaPlayerProperty( "mp-pause", { 32 | MenuLabel = "Pause", 33 | MenuIcon = "icon16/control_pause_blue.png", 34 | 35 | Filter = function( self, ent, ply ) 36 | if not IsPrivilegedMediaPlayer(self, ent, ply) then return end 37 | local mp = ent:GetMediaPlayer() 38 | return IsValid(mp) and mp:GetPlayerState() == MP_STATE_PLAYING 39 | end, 40 | 41 | Action = function( self, ent ) 42 | MediaPlayer.Pause( ent ) 43 | end 44 | }) 45 | 46 | AddMediaPlayerProperty( "mp-resume", { 47 | MenuLabel = "Resume", 48 | MenuIcon = "icon16/control_play_blue.png", 49 | 50 | Filter = function( self, ent, ply ) 51 | if not IsPrivilegedMediaPlayer(self, ent, ply) then return end 52 | local mp = ent:GetMediaPlayer() 53 | return IsValid(mp) and mp:GetPlayerState() == MP_STATE_PAUSED 54 | end, 55 | 56 | Action = function( self, ent ) 57 | MediaPlayer.Pause( ent ) 58 | end 59 | }) 60 | 61 | AddMediaPlayerProperty( "mp-skip", { 62 | MenuLabel = "Skip", 63 | MenuIcon = "icon16/control_end_blue.png", 64 | 65 | Filter = function( self, ent, ply ) 66 | if not IsPrivilegedMediaPlayer(self, ent, ply) then return end 67 | local mp = ent:GetMediaPlayer() 68 | return IsValid(mp) and HasMedia(mp) 69 | end, 70 | 71 | Action = function( self, ent ) 72 | MediaPlayer.Skip( ent ) 73 | end 74 | }) 75 | 76 | AddMediaPlayerProperty( "mp-seek", { 77 | MenuLabel = "Seek", 78 | -- MenuIcon = "icon16/timeline_marker.png", 79 | MenuIcon = "icon16/control_fastforward_blue.png", 80 | 81 | Filter = function( self, ent, ply ) 82 | if not IsPrivilegedMediaPlayer(self, ent, ply) then return end 83 | local mp = ent:GetMediaPlayer() 84 | return IsValid(mp) and HasMedia(mp) 85 | end, 86 | 87 | Action = function( self, ent ) 88 | 89 | Derma_StringRequest( 90 | "Media Player", 91 | "Enter a time in HH:MM:SS format (hours, minutes, seconds):", 92 | "", -- Default text 93 | function( time ) 94 | MediaPlayer.Seek( ent, time ) 95 | end, 96 | function() end, 97 | "Seek", 98 | "Cancel" 99 | ) 100 | 101 | end 102 | }) 103 | 104 | AddMediaPlayerProperty( "mp-request-url", { 105 | MenuLabel = "Request URL", 106 | MenuIcon = "icon16/link_add.png", 107 | Filter = IsMediaPlayer, 108 | 109 | Action = function( self, ent ) 110 | 111 | MediaPlayer.OpenRequestMenu( ent ) 112 | 113 | end 114 | }) 115 | 116 | AddMediaPlayerProperty( "mp-copy-url", { 117 | MenuLabel = "Copy URL to clipboard", 118 | MenuIcon = "icon16/paste_plain.png", 119 | 120 | Filter = function( self, ent, ply ) 121 | if not IsMediaPlayer(self, ent, ply) then return end 122 | local mp = ent:GetMediaPlayer() 123 | return IsValid(mp) and HasMedia(mp) 124 | end, 125 | 126 | Action = function( self, ent ) 127 | 128 | local mp = ent:GetMediaPlayer() 129 | local media = mp and mp:CurrentMedia() 130 | if not IsValid(media) then return end 131 | 132 | SetClipboardText( media:Url() ) 133 | LocalPlayer():ChatPrint( "Media URL has been copied into your clipboard." ) 134 | 135 | end 136 | }) 137 | 138 | AddMediaPlayerProperty( "mp-enable", { 139 | MenuLabel = "Turn On", 140 | MenuIcon = "icon16/lightbulb.png", 141 | 142 | Filter = function( self, ent, ply ) 143 | return IsValid(ent) and IsValid(ply) and 144 | ent.IsMediaPlayerEntity and 145 | not IsValid(ent:GetMediaPlayer()) and 146 | gamemode.Call( "CanProperty", ply, self.InternalName, ent ) 147 | end, 148 | 149 | Action = function( self, ent ) 150 | MediaPlayer.RequestListen( ent ) 151 | end 152 | }) 153 | 154 | AddMediaPlayerProperty( "mp-disable", { 155 | MenuLabel = "Turn Off", 156 | MenuIcon = "icon16/lightbulb_off.png", 157 | 158 | Filter = function( self, ent, ply ) 159 | return IsValid(ent) and IsValid(ply) and 160 | ent.IsMediaPlayerEntity and 161 | IsValid(ent:GetMediaPlayer()) and 162 | gamemode.Call( "CanProperty", ply, self.InternalName, ent ) 163 | end, 164 | 165 | Action = function( self, ent ) 166 | MediaPlayer.RequestListen( ent ) 167 | end 168 | }) 169 | -------------------------------------------------------------------------------- /lua/entities/mediaplayer_base/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | -------------------------------------------------------------------------------- /lua/entities/mediaplayer_base/init.lua: -------------------------------------------------------------------------------- 1 | if SERVER then 2 | AddCSLuaFile "shared.lua" 3 | AddCSLuaFile "cl_init.lua" 4 | 5 | resource.AddFile "materials/theater/STATIC.vmt" 6 | end 7 | include "shared.lua" 8 | 9 | ENT.UseDelay = 0.5 -- seconds 10 | 11 | function ENT:Use(ply) 12 | if not IsValid(ply) then return end 13 | 14 | -- Delay request 15 | if ply.NextUse and ply.NextUse > CurTime() then 16 | return 17 | end 18 | 19 | local mp = self:GetMediaPlayer() 20 | 21 | if not mp then 22 | ErrorNoHalt("MediaPlayer test entity doesn't have player installed\n") 23 | debug.Trace() 24 | return 25 | end 26 | 27 | if mp:HasListener(ply) then 28 | mp:RemoveListener(ply) 29 | else 30 | mp:AddListener(ply) 31 | end 32 | 33 | ply.NextUse = CurTime() + self.UseDelay 34 | end 35 | 36 | function ENT:UpdateTransmitState() 37 | return TRANSMIT_PVS 38 | end 39 | 40 | function ENT:OnEntityCopyTableFinish( data ) 41 | local mp = self:GetMediaPlayer() 42 | data.MediaPlayerSnapshot = mp:GetSnapshot() 43 | data._mp = nil 44 | end 45 | 46 | function ENT:PostEntityPaste( ply, ent, createdEnts ) 47 | local snapshot = self.MediaPlayerSnapshot 48 | if not snapshot then return end 49 | 50 | local mp = self:GetMediaPlayer() 51 | self:SetMediaPlayerID( mp:GetId() ) 52 | 53 | mp:RestoreSnapshot( snapshot ) 54 | 55 | self.MediaPlayerSnapshot = nil 56 | end 57 | 58 | function ENT:KeyValue( key, value ) 59 | if key == "model" then 60 | self.Model = value 61 | end 62 | end 63 | 64 | function ENT:AcceptInput( name, activator, caller, data ) 65 | local mp = self:GetMediaPlayer() 66 | if not IsValid(mp) then return false end 67 | 68 | local ply = IsValid(activator) and activator:IsPlayer() and activator 69 | 70 | if name == "AddPlayer" then 71 | if ply and not mp:HasListener(ply) then 72 | mp:AddListener(ply) 73 | end 74 | elseif name == "RemovePlayer" then 75 | if ply and mp:HasListener(ply) then 76 | mp:RemoveListener(ply) 77 | end 78 | elseif name == "RemoveAllPlayers" then 79 | mp:SetListeners({}) 80 | elseif name == "PlayPauseMedia" then 81 | mp:PlayPause() 82 | elseif name == "SkipMedia" then 83 | mp:OnMediaFinished() 84 | elseif name == "ClearMedia" then 85 | mp:ClearMediaQueue() 86 | mp:OnMediaFinished() 87 | else 88 | return false 89 | end 90 | 91 | return true 92 | end 93 | -------------------------------------------------------------------------------- /lua/entities/mediaplayer_base/shared.lua: -------------------------------------------------------------------------------- 1 | ENT.Type = "anim" 2 | ENT.Base = "base_anim" 3 | 4 | ENT.Spawnable = false 5 | 6 | ENT.Model = Model( "models/props_phx/rt_screen.mdl" ) 7 | 8 | ENT.MediaPlayerType = "entity" 9 | ENT.IsMediaPlayerEntity = true 10 | 11 | local ErrorModel = "models/error.mdl" 12 | 13 | function ENT:Initialize() 14 | 15 | if SERVER then 16 | if self:GetModel() == ErrorModel then 17 | self:SetModel( self.Model ) 18 | end 19 | 20 | self:SetUseType( SIMPLE_USE ) 21 | 22 | self:PhysicsInit( SOLID_VPHYSICS ) 23 | self:SetMoveType( MOVETYPE_VPHYSICS ) 24 | 25 | local phys = self:GetPhysicsObject() 26 | if IsValid( phys ) then 27 | phys:EnableMotion( false ) 28 | end 29 | 30 | -- Install media player to entity 31 | local mp = self:InstallMediaPlayer( self.MediaPlayerType ) 32 | 33 | -- Network media player ID 34 | self:SetMediaPlayerID( mp:GetId() ) 35 | end 36 | 37 | -- Apply player config based on model 38 | self.PlayerConfig = self:GetMediaPlayerConfig() 39 | end 40 | 41 | function ENT:SetupDataTables() 42 | self:NetworkVar( "String", 0, "MediaPlayerID" ) 43 | end 44 | 45 | function ENT:OnRemove() 46 | local mp = self:GetMediaPlayer() 47 | if mp then 48 | mp:Remove() 49 | end 50 | end 51 | 52 | function ENT:GetMediaPlayerConfig() 53 | local model = self:GetModel() 54 | local MPModelConfigs = list.Get( "MediaPlayerModelConfigs" ) 55 | local config = MPModelConfigs and MPModelConfigs[model] or self.PlayerConfig 56 | return config 57 | end 58 | -------------------------------------------------------------------------------- /lua/entities/mediaplayer_tv/shared.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile() 2 | 3 | if SERVER then 4 | resource.AddFile( "models/gmod_tower/suitetv_large.mdl" ) 5 | resource.AddFile( "materials/models/gmod_tower/suitetv_large.vmt" ) 6 | resource.AddSingleFile( "materials/entities/mediaplayer_tv.png" ) 7 | end 8 | 9 | DEFINE_BASECLASS( "mediaplayer_base" ) 10 | 11 | ENT.PrintName = "Big Screen TV" 12 | ENT.Author = "Samuel Maddock" 13 | ENT.Instructions = "Right click on the TV to see available Media Player options. Alternatively, press E on the TV to turn it on." 14 | ENT.Category = "Media Player" 15 | 16 | ENT.Type = "anim" 17 | ENT.Base = "mediaplayer_base" 18 | 19 | ENT.Spawnable = true 20 | 21 | ENT.Model = Model( "models/gmod_tower/suitetv_large.mdl" ) 22 | 23 | list.Set( "MediaPlayerModelConfigs", ENT.Model, { 24 | angle = Angle(-90, 90, 0), 25 | offset = Vector(6, 59.49, 103.65), 26 | width = 119, 27 | height = 69 28 | } ) 29 | 30 | function ENT:SetupDataTables() 31 | BaseClass.SetupDataTables( self ) 32 | 33 | self:NetworkVar( "String", 1, "MediaThumbnail" ) 34 | end 35 | 36 | if SERVER then 37 | 38 | function ENT:SetupMediaPlayer( mp ) 39 | mp:on("mediaChanged", function(media) self:OnMediaChanged(media) end) 40 | end 41 | 42 | function ENT:OnMediaChanged( media ) 43 | self:SetMediaThumbnail( media and media:Thumbnail() or "" ) 44 | end 45 | 46 | else -- CLIENT 47 | 48 | local draw = draw 49 | local surface = surface 50 | local Start3D2D = cam.Start3D2D 51 | local End3D2D = cam.End3D2D 52 | local DrawHTMLMaterial = DrawHTMLMaterial 53 | 54 | local TEXT_ALIGN_CENTER = TEXT_ALIGN_CENTER 55 | local color_white = color_white 56 | 57 | local StaticMaterial = Material( "theater/STATIC" ) 58 | local TextScale = 700 59 | 60 | function ENT:Draw() 61 | self:DrawModel() 62 | 63 | local mp = self:GetMediaPlayer() 64 | 65 | if not mp then 66 | self:DrawMediaPlayerOff() 67 | end 68 | end 69 | 70 | local HTMLMAT_STYLE_ARTWORK_BLUR = 'htmlmat.style.artwork_blur' 71 | AddHTMLMaterialStyle( HTMLMAT_STYLE_ARTWORK_BLUR, { 72 | width = 720, 73 | height = 480 74 | }, HTMLMAT_STYLE_BLUR ) 75 | 76 | local DrawThumbnailsCvar = MediaPlayer.Cvars.DrawThumbnails 77 | 78 | function ENT:DrawMediaPlayerOff() 79 | local w, h, pos, ang = self:GetMediaPlayerPosition() 80 | local thumbnail = self:GetMediaThumbnail() 81 | 82 | Start3D2D( pos, ang, 1 ) 83 | if DrawThumbnailsCvar:GetBool() and thumbnail != "" then 84 | DrawHTMLMaterial( thumbnail, HTMLMAT_STYLE_ARTWORK_BLUR, w, h ) 85 | else 86 | surface.SetDrawColor( color_white ) 87 | surface.SetMaterial( StaticMaterial ) 88 | surface.DrawTexturedRect( 0, 0, w, h ) 89 | end 90 | End3D2D() 91 | 92 | 93 | local scale = w / TextScale 94 | Start3D2D( pos, ang, scale ) 95 | local tw, th = w / scale, h / scale 96 | draw.SimpleText( "Press E to begin watching", "MediaTitle", 97 | tw/2, th/2, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER ) 98 | End3D2D() 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /lua/mediaplayer/cl_idlescreen.lua: -------------------------------------------------------------------------------- 1 | local DefaultIdlescreen = [[ 2 | 3 | 4 | 5 | 6 | MediaPlayer Idlescreen 7 | 79 | 80 | 81 | 82 |
83 |

No media playing

84 |

Hold %s while looking at the media player to reveal the queue menu.

85 | 86 |
87 | Hey Media Player fans! The creator of this mod is making something new. 88 | Check out getmetastream.com! 89 |
90 |
91 | 92 | 93 | ]] 94 | 95 | local function GetIdlescreenHTML() 96 | local contextMenuBind = input.LookupBinding( "+menu_context" ) or "C" 97 | contextMenuBind = contextMenuBind:upper() 98 | return DefaultIdlescreen:format( contextMenuBind ) 99 | end 100 | 101 | function MediaPlayer.GetIdlescreen() 102 | 103 | if not MediaPlayer._idlescreen then 104 | local browser = vgui.Create( "DMediaPlayerHTML" ) 105 | browser:SetPaintedManually(true) 106 | browser:SetKeyBoardInputEnabled(false) 107 | browser:SetMouseInputEnabled(false) 108 | browser:SetPos(0,0) 109 | 110 | local resolution = MediaPlayer.Resolution() 111 | browser:SetSize( resolution * 16/9, resolution ) 112 | 113 | -- TODO: set proper browser size 114 | 115 | MediaPlayer._idlescreen = browser 116 | 117 | local setup = hook.Run( "MediaPlayerSetupIdlescreen", browser ) 118 | if not setup then 119 | MediaPlayer._idlescreen:SetHTML( GetIdlescreenHTML() ) 120 | end 121 | end 122 | 123 | return MediaPlayer._idlescreen 124 | 125 | end 126 | -------------------------------------------------------------------------------- /lua/mediaplayer/cl_init.lua: -------------------------------------------------------------------------------- 1 | if MediaPlayer then 2 | -- TODO: compare versions? 3 | if MediaPlayer.__refresh then 4 | MediaPlayer.__refresh = nil 5 | else 6 | return -- MediaPlayer has already been registered 7 | end 8 | end 9 | 10 | include "controls/dmediaplayerhtml.lua" 11 | include "controls/dhtmlcontrols.lua" 12 | include "controls/dmediaplayerrequest.lua" 13 | include "shared.lua" 14 | include "cl_requests.lua" 15 | include "cl_idlescreen.lua" 16 | include "cl_screen.lua" 17 | 18 | function MediaPlayer.Volume( volume ) 19 | 20 | local cur = MediaPlayer.Cvars.Volume:GetFloat() 21 | 22 | if volume then 23 | 24 | -- Normalize volume 25 | volume = volume > 1 and volume/100 or volume 26 | 27 | -- Set volume convar 28 | RunConsoleCommand( "mediaplayer_volume", volume ) 29 | 30 | -- Apply volume to all media players 31 | for _, mp in pairs( MediaPlayer.List ) do 32 | if mp:IsPlaying() then 33 | local media = mp:CurrentMedia() 34 | if media then 35 | media:Volume( volume ) 36 | end 37 | end 38 | end 39 | 40 | hook.Run( MP.EVENTS.VOLUME_CHANGED, volume, cur ) 41 | 42 | cur = volume 43 | 44 | end 45 | 46 | return cur 47 | 48 | end 49 | 50 | local muted = false 51 | local previousVolume 52 | function MediaPlayer.ToggleMute() 53 | if not muted then 54 | previousVolume = MediaPlayer.Volume() 55 | end 56 | 57 | local vol = muted and previousVolume or 0 58 | MediaPlayer.Volume( vol ) 59 | muted = not muted 60 | end 61 | 62 | function MediaPlayer.Resolution( resolution ) 63 | 64 | if resolution then 65 | resolution = math.Clamp( resolution, 16, 4096 ) 66 | RunConsoleCommand( "mediaplayer_resolution", resolution ) 67 | end 68 | 69 | return MediaPlayer.Cvars.Resolution:GetFloat() 70 | 71 | end 72 | 73 | 74 | --[[--------------------------------------------------------- 75 | Utility functions 76 | -----------------------------------------------------------]] 77 | 78 | local FullscreenCvar = MediaPlayer.Cvars.Fullscreen 79 | 80 | function MediaPlayer.SetBrowserSize( browser, w, h ) 81 | 82 | local fullscreen = FullscreenCvar:GetBool() 83 | 84 | if fullscreen then 85 | w, h = ScrW(), ScrH() 86 | end 87 | 88 | browser:SetSize( w, h, fullscreen ) 89 | 90 | end 91 | 92 | function MediaPlayer.OpenRequestMenu( mp ) 93 | 94 | if ValidPanel(MediaPlayer._RequestMenu) then 95 | return 96 | end 97 | 98 | mp = MediaPlayer.GetByObject( mp ) 99 | 100 | if not mp then 101 | Error( "MediaPlayer.OpenRequestMenu: Invalid media player.\n" ) 102 | return 103 | end 104 | 105 | local req = vgui.Create( "MPRequestFrame" ) 106 | req:SetMediaPlayer( mp ) 107 | req:MakePopup() 108 | req:Center() 109 | 110 | req.OnClose = function() 111 | MediaPlayer._RequestMenu = nil 112 | end 113 | 114 | MediaPlayer._RequestMenu = req 115 | 116 | end 117 | 118 | function MediaPlayer.MenuRequest( url ) 119 | 120 | local menu = MediaPlayer._RequestMenu 121 | 122 | if not ValidPanel(menu) then 123 | return 124 | end 125 | 126 | local mp = menu:GetMediaPlayer() 127 | 128 | menu:Close() 129 | 130 | MediaPlayer.Request( mp, url ) 131 | 132 | end 133 | 134 | 135 | --[[--------------------------------------------------------- 136 | Fonts 137 | -----------------------------------------------------------]] 138 | 139 | local common = { 140 | -- font = "Open Sans Condensed", 141 | -- font = "Oswald", 142 | font = "Clear Sans Medium", 143 | antialias = true, 144 | weight = 400 145 | } 146 | 147 | surface.CreateFont( "MediaTitle", table.Merge(common, { size = 72 }) ) 148 | surface.CreateFont( "MediaRequestButton", table.Merge(common, { size = 26 }) ) 149 | -------------------------------------------------------------------------------- /lua/mediaplayer/cl_requests.lua: -------------------------------------------------------------------------------- 1 | local function GetMediaPlayerId( obj ) 2 | local mpId 3 | 4 | -- Determine mp parameter type and get the associated ID. 5 | if isentity(obj) and obj.IsMediaPlayerEntity then 6 | mpId = obj:GetMediaPlayerID() 7 | -- elseif isentity(obj) and IsValid( obj:GetMediaPlayer() ) then 8 | -- local mp = mp:GetMediaPlayer() 9 | -- mpId = mp:GetId() 10 | elseif istable(obj) and obj.IsMediaPlayer then 11 | mpId = obj:GetId() 12 | elseif isstring(obj) then 13 | mpId = obj 14 | else 15 | return false -- Invalid parameters 16 | end 17 | 18 | return mpId 19 | end 20 | 21 | --- 22 | -- Request to begin listening to a media player. 23 | -- 24 | -- @param Entity|Table|String Media player reference. 25 | -- 26 | function MediaPlayer.RequestListen( obj ) 27 | 28 | local mpId = GetMediaPlayerId(obj) 29 | if not mpId then return end 30 | 31 | net.Start( "MEDIAPLAYER.RequestListen" ) 32 | net.WriteString( mpId ) 33 | net.SendToServer() 34 | 35 | end 36 | 37 | --- 38 | -- Request mediaplayer update. 39 | -- 40 | -- @param Entity|Table|String Media player reference. 41 | -- 42 | function MediaPlayer.RequestUpdate( obj ) 43 | 44 | local mpId = GetMediaPlayerId(obj) 45 | if not mpId then return end 46 | 47 | net.Start( "MEDIAPLAYER.RequestUpdate" ) 48 | net.WriteString( mpId ) 49 | net.SendToServer() 50 | 51 | end 52 | 53 | --- 54 | -- Request a URL to be played on the given media player. 55 | -- 56 | -- @param Entity|Table|String Media player reference. 57 | -- @param String Requested media URL. 58 | -- 59 | function MediaPlayer.Request( obj, url ) 60 | 61 | local mpId = GetMediaPlayerId( obj ) 62 | if not mpId then return end 63 | 64 | if MediaPlayer.DEBUG then 65 | print("MEDIAPLAYER.Request:", url, mpId) 66 | end 67 | 68 | local mp = MediaPlayer.GetById( mpId ) 69 | 70 | local allowWebpage = MediaPlayer.Cvars.AllowWebpages:GetBool() 71 | 72 | -- Verify valid URL as to not waste time networking 73 | if not MediaPlayer.ValidUrl( url ) and not allowWebpage then 74 | LocalPlayer():ChatPrint("The requested URL was invalid.") 75 | return false 76 | end 77 | 78 | local media = MediaPlayer.GetMediaForUrl( url, allowWebpage ) 79 | 80 | local function request( err ) 81 | if err then 82 | -- TODO: don't use chatprint to notify the user 83 | LocalPlayer():ChatPrint( "Request failed: " .. err ) 84 | return 85 | end 86 | 87 | if not IsValid( mp ) then 88 | -- media player may have been removed before we could finish the 89 | -- async prerequest action 90 | return 91 | end 92 | 93 | net.Start( "MEDIAPLAYER.RequestMedia" ) 94 | net.WriteString( mpId ) 95 | net.WriteString( url ) 96 | media:NetWriteRequest() -- send any additional data 97 | net.SendToServer() 98 | 99 | if MediaPlayer.DEBUG then 100 | print("MEDIAPLAYER.Request sent to server") 101 | end 102 | end 103 | 104 | -- Prepare any data prior to requesting if necessary 105 | if media.PrefetchMetadata then 106 | media:PreRequest(request) -- async 107 | else 108 | request() -- sync 109 | end 110 | 111 | end 112 | 113 | function MediaPlayer.Pause( mp ) 114 | 115 | local mpId = GetMediaPlayerId( mp ) 116 | if not mpId then return end 117 | 118 | net.Start( "MEDIAPLAYER.RequestPause" ) 119 | net.WriteString( mpId ) 120 | net.SendToServer() 121 | 122 | end 123 | 124 | function MediaPlayer.Skip( mp ) 125 | 126 | local mpId = GetMediaPlayerId( mp ) 127 | if not mpId then return end 128 | 129 | net.Start( "MEDIAPLAYER.RequestSkip" ) 130 | net.WriteString( mpId ) 131 | net.SendToServer() 132 | 133 | end 134 | 135 | --- 136 | -- Seek to a specific time in the current media. 137 | -- 138 | -- @param Entity|Table|String Media player reference. 139 | -- @param String Seek time; HH:MM:SS 140 | -- 141 | function MediaPlayer.Seek( mp, time ) 142 | 143 | local mpId = GetMediaPlayerId( mp ) 144 | if not mpId then return end 145 | 146 | -- always convert to time in seconds before sending 147 | if type(time) == 'string' then 148 | time = MediaPlayerUtils.ParseHHMMSS(time) or 0 149 | end 150 | 151 | net.Start( "MEDIAPLAYER.RequestSeek" ) 152 | net.WriteString( mpId ) 153 | net.WriteInt( time, 32 ) 154 | net.SendToServer() 155 | 156 | end 157 | 158 | --- 159 | -- Remove the given media. 160 | -- 161 | -- @param Entity|Table|String Media player reference. 162 | -- @param String Media unique ID. 163 | -- 164 | function MediaPlayer.RequestRemove( mp, mediaUID ) 165 | 166 | local mpId = GetMediaPlayerId( mp ) 167 | if not mpId then return end 168 | 169 | net.Start( "MEDIAPLAYER.RequestRemove" ) 170 | net.WriteString( mpId ) 171 | net.WriteString( mediaUID ) 172 | net.SendToServer() 173 | 174 | end 175 | 176 | function MediaPlayer.RequestRepeat( mp ) 177 | 178 | local mpId = GetMediaPlayerId( mp ) 179 | if not mpId then return end 180 | 181 | net.Start( "MEDIAPLAYER.RequestRepeat" ) 182 | net.WriteString( mpId ) 183 | net.SendToServer() 184 | 185 | end 186 | 187 | function MediaPlayer.RequestShuffle( mp ) 188 | 189 | local mpId = GetMediaPlayerId( mp ) 190 | if not mpId then return end 191 | 192 | net.Start( "MEDIAPLAYER.RequestShuffle" ) 193 | net.WriteString( mpId ) 194 | net.SendToServer() 195 | 196 | end 197 | 198 | function MediaPlayer.RequestLock( mp ) 199 | 200 | local mpId = GetMediaPlayerId( mp ) 201 | if not mpId then return end 202 | 203 | net.Start( "MEDIAPLAYER.RequestLock" ) 204 | net.WriteString( mpId ) 205 | net.SendToServer() 206 | 207 | end 208 | -------------------------------------------------------------------------------- /lua/mediaplayer/cl_screen.lua: -------------------------------------------------------------------------------- 1 | --[[--------------------------------------------------------- 2 | Pass mouse clicks into media player browser 3 | -----------------------------------------------------------]] 4 | 5 | local MAX_SCREEN_DISTANCE = 1000 6 | 7 | local function getScreenPos( ent, aimVector ) 8 | local w, h, pos, ang = ent:GetMediaPlayerPosition() 9 | local eyePos = LocalPlayer():EyePos() 10 | 11 | if pos:Distance( eyePos ) > MAX_SCREEN_DISTANCE then 12 | return 13 | end 14 | 15 | local screenNormal = ang:Up() 16 | 17 | if screenNormal:Dot( aimVector ) > 0 then 18 | return -- prevent clicks from behind the screen 19 | end 20 | 21 | local hitPos = util.IntersectRayWithPlane( 22 | eyePos, 23 | aimVector, 24 | pos, 25 | screenNormal 26 | ) 27 | 28 | if not hitPos then 29 | return 30 | end 31 | 32 | if MediaPlayer.DEBUG then 33 | debugoverlay.Cross( hitPos, 1, 60 ) 34 | end 35 | 36 | local localPos = WorldToLocal( pos, ang, hitPos, ang ) 37 | local x, y = -localPos.x, localPos.y 38 | 39 | if ( x < 0 or x > w ) or ( y < 0 or y > h ) then 40 | return -- out of screen bounds 41 | end 42 | 43 | return x / w, y / h 44 | end 45 | 46 | function MediaPlayer.DispatchScreenTrace( func, aimVector ) 47 | if type(func) ~= "function" then return end 48 | if not aimVector then 49 | aimVector = LocalPlayer():GetAimVector() 50 | end 51 | 52 | for name, mp in pairs( MediaPlayer.List ) do 53 | local ent = mp.Entity 54 | if IsValid( mp ) and not ent:IsDormant() then 55 | local x, y = getScreenPos( ent, aimVector ) 56 | if x and y then 57 | func(mp, x, y) 58 | end 59 | end 60 | end 61 | end 62 | 63 | local function mpMouseReleased( mp, x, y ) 64 | mp:OnMousePressed(x, y) 65 | end 66 | 67 | local function mousePressed( mouseCode, aimVector ) 68 | if mouseCode ~= MOUSE_LEFT then 69 | return 70 | end 71 | 72 | MediaPlayer.DispatchScreenTrace( mpMouseReleased, aimVector ) 73 | end 74 | hook.Add( "GUIMouseReleased", "MediaPlayer.ScreenIntersect", mousePressed ) 75 | 76 | 77 | --[[--------------------------------------------------------- 78 | Pass mouse scrolling into media player browser 79 | -----------------------------------------------------------]] 80 | 81 | local mouseScroll = MediaPlayerUtils.Throttle(function( dt ) 82 | MediaPlayer.DispatchScreenTrace(function(mp) 83 | mp:OnMouseWheeled(dt) 84 | end, aimVector) 85 | end, 0.01, { trailing = false }) 86 | 87 | hook.Add( "ContextMenuCreated", "MediaPlayer.Scroll", function( contextMenu ) 88 | if contextMenu.OnMouseWheeled then return end 89 | contextMenu.OnMouseWheeled = function(panel, scrollDelta) 90 | mouseScroll(scrollDelta) 91 | end 92 | end ) 93 | 94 | --[[ 95 | local function checkMouseScroll( ply, cmd ) 96 | local scrollDelta = cmd:GetMouseWheel() 97 | if scrollDelta == 0 then return end 98 | mouseScroll(scrollDelta) 99 | end 100 | hook.Add( "StartCommand", "MediaPlayer.Scroll", checkMouseScroll ) 101 | ]] 102 | 103 | --[[--------------------------------------------------------- 104 | Prevent weapons from firing while the context menu is 105 | open and the cursor is aiming at a screen. 106 | -----------------------------------------------------------]] 107 | 108 | local function isAimingAtScreen() 109 | local aimVector = LocalPlayer():GetAimVector() 110 | for name, mp in pairs( MediaPlayer.List ) do 111 | local ent = mp.Entity 112 | if IsValid( mp ) and not ent:IsDormant() then 113 | local x, y = getScreenPos( ent, aimVector ) 114 | if x then 115 | return true 116 | end 117 | end 118 | end 119 | end 120 | 121 | local function preventWorldClicker() 122 | local ply = LocalPlayer() 123 | 124 | if not ply:IsWorldClicking() then return end 125 | 126 | local ent = ply:GetEyeTrace().Entity 127 | if not ( IsValid(ent) and ent.IsMediaPlayerEntity ) then return end 128 | 129 | if isAimingAtScreen() then 130 | return true 131 | end 132 | end 133 | hook.Add( "PreventScreenClicks", "MediaPlayer.PreventWorldClicker", preventWorldClicker ) 134 | -------------------------------------------------------------------------------- /lua/mediaplayer/config/client.lua: -------------------------------------------------------------------------------- 1 | --[[---------------------------------------------------------------------------- 2 | Media Player client configuration 3 | ------------------------------------------------------------------------------]] 4 | MediaPlayer.SetConfig({ 5 | 6 | --- 7 | -- HTML content 8 | -- 9 | html = { 10 | 11 | --- 12 | -- Base URL where HTML content is located. 13 | -- @type String 14 | -- 15 | base_url = "http://samuelmaddock.github.io/gm-mediaplayer/" 16 | 17 | }, 18 | 19 | --- 20 | -- Request menu 21 | -- 22 | request = { 23 | 24 | --- 25 | -- URL of the request menu. 26 | -- @type String 27 | -- 28 | url = "http://samuelmaddock.github.io/gm-mediaplayer/request.html" 29 | 30 | } 31 | 32 | }) 33 | -------------------------------------------------------------------------------- /lua/mediaplayer/config/server.lua: -------------------------------------------------------------------------------- 1 | --[[---------------------------------------------------------------------------- 2 | The Media Player server configuration contains API keys used for requesting 3 | metadata for various services. All keys provided with the addon should not 4 | be used elsewhere as to respect data usage limits. 5 | ------------------------------------------------------------------------------]] 6 | MediaPlayer.SetConfig({ 7 | 8 | --[[------------------------------------------------------------------------ 9 | Google's Data API is used for YouTube and GoogleDrive requests. To 10 | get your own API key, read through the following guide: 11 | https://developers.google.com/youtube/v3/getting-started#intro 12 | --------------------------------------------------------------------------]] 13 | google = { 14 | ["api_key"] = "AIzaSyAjSwUHzyoxhfQZmiSqoIBQpawm2ucF11E", 15 | ["referrer"] = "http://mediaplayer.pixeltailgames.com/" 16 | }, 17 | 18 | --[[------------------------------------------------------------------------ 19 | SoundCloud API 20 | 21 | To register your own application, use the following webpage: 22 | http://soundcloud.com/you/apps/new 23 | --------------------------------------------------------------------------]] 24 | soundcloud = { 25 | ["client_id"] = "2e0e541854cbabd873d647c1d45f79e8" 26 | }, 27 | 28 | --[[------------------------------------------------------------------------ 29 | Twitch Developer Application 30 | 31 | To register your own application, see the following webpage: 32 | https://dev.twitch.tv/docs/v5/guides/using-the-twitch-api 33 | --------------------------------------------------------------------------]] 34 | twitch = { 35 | client_id = "cg1n8y5akizthcctygugthfq94tsg3" 36 | } 37 | 38 | }) 39 | -------------------------------------------------------------------------------- /lua/mediaplayer/controls/dmediaplayerrequest.lua: -------------------------------------------------------------------------------- 1 | local PANEL = {} 2 | PANEL.HistoryWidth = 300 3 | PANEL.BackgroundColor = Color(22, 22, 22) 4 | 5 | local CloseTexture = Material( "theater/close.png" ) 6 | 7 | AccessorFunc( PANEL, "m_MediaPlayer", "MediaPlayer" ) 8 | 9 | function PANEL:Init() 10 | 11 | self:SetPaintBackgroundEnabled( true ) 12 | self:SetFocusTopLevel( true ) 13 | 14 | local w = math.Clamp( ScrW() - 100, 800, 1152 + self.HistoryWidth ) 15 | local h = ScrH() 16 | if h > 800 then 17 | h = h * 3/4 18 | elseif h > 600 then 19 | h = h * 7/8 20 | end 21 | self:SetSize( w, h ) 22 | 23 | self.CloseButton = vgui.Create( "DIconButton", self ) 24 | self.CloseButton:SetSize( 32, 32 ) 25 | self.CloseButton:SetIcon( "mp-close" ) 26 | self.CloseButton:SetColor( Color( 250, 250, 250, 200 ) ) 27 | self.CloseButton:SetZPos( 5 ) 28 | self.CloseButton:SetText( "" ) 29 | self.CloseButton.DoClick = function ( button ) 30 | self:Close() 31 | end 32 | 33 | self.BrowserContainer = vgui.Create( "DPanel", self ) 34 | self.BrowserContainer:Dock( FILL ) 35 | 36 | self.Browser = vgui.Create( "DMediaPlayerHTML", self.BrowserContainer ) 37 | self.Browser:Dock( FILL ) 38 | 39 | self.Browser:AddFunction( "gmod", "requestUrl", function (url) 40 | MediaPlayer.MenuRequest( url ) 41 | self:Close() 42 | end ) 43 | 44 | self.Browser:AddFunction( "gmod", "openUrl", function (url) 45 | gui.OpenURL( url ) 46 | end ) 47 | 48 | self.Browser:AddFunction( "gmod", "getServices", function () 49 | local mp = self.m_MediaPlayer 50 | 51 | if mp then 52 | self:SendServices( mp ) 53 | end 54 | end ) 55 | 56 | local requestUrl = MediaPlayer.GetConfigValue( 'request.url' ) 57 | self.Browser:OpenURL( requestUrl ) 58 | 59 | self.Controls = vgui.Create( "MPHTMLControls", self.BrowserContainer ) 60 | self.Controls:Dock( TOP ) 61 | self.Controls:DockPadding( 0, 0, 32, 0 ) 62 | self.Controls:SetHTML( self.Browser ) 63 | self.Controls.BorderSize = 0 64 | 65 | -- Listen for all mouse press events 66 | hook.Add( "GUIMousePressed", self, self.OnGUIMousePressed ) 67 | 68 | end 69 | 70 | local function GetServiceIDs( mp ) 71 | -- Send list of supported services to the request page for filtering out 72 | -- service icons 73 | local serviceIDs = mp:GetSupportedServiceIDs() 74 | serviceIDs = table.concat( serviceIDs, "," ) 75 | 76 | return serviceIDs 77 | end 78 | 79 | function PANEL:SendServices( mp ) 80 | local js = "if (typeof window.setServices === 'function') { setServices('%s'); }" 81 | js = js:format( GetServiceIDs(mp) ) 82 | 83 | self.Browser:RunJavascript( js ) 84 | self.Browser:QueueJavascript( js ) 85 | end 86 | 87 | function PANEL:SetMediaPlayer( mp ) 88 | self.m_MediaPlayer = mp 89 | 90 | self:SendServices( mp ) 91 | end 92 | 93 | function PANEL:Paint( w, h ) 94 | 95 | -- Draw background for fully transparent webpages 96 | surface.SetDrawColor( self.BackgroundColor ) 97 | surface.DrawRect( 0, 0, w, h ) 98 | 99 | return true 100 | 101 | end 102 | 103 | function PANEL:OnRemove() 104 | hook.Remove( "GUIMousePressed", self ) 105 | end 106 | 107 | function PANEL:Close() 108 | if ValidPanel(self.Browser) then 109 | self.Browser:Remove() 110 | end 111 | 112 | self:OnClose() 113 | self:Remove() 114 | end 115 | 116 | function PANEL:OnClose() 117 | 118 | end 119 | 120 | function PANEL:CheckClose() 121 | 122 | local x, y = self:CursorPos() 123 | 124 | -- Remove panel if mouse is clicked outside of itself 125 | if not (gui.IsGameUIVisible() or gui.IsConsoleVisible()) and 126 | ( x < 0 or x > self:GetWide() or y < 0 or y > self:GetTall() ) then 127 | self:Close() 128 | end 129 | 130 | end 131 | 132 | function PANEL:PerformLayout( w, h ) 133 | 134 | self.CloseButton:SetPos( w - 36, 2 ) 135 | 136 | end 137 | 138 | --- 139 | -- Close the panel when the mouse has been pressed outside of the panel. 140 | -- 141 | function PANEL:OnGUIMousePressed( key ) 142 | 143 | if key == MOUSE_LEFT then 144 | self:CheckClose() 145 | end 146 | 147 | end 148 | 149 | vgui.Register( "MPRequestFrame", PANEL, "EditablePanel" ) 150 | -------------------------------------------------------------------------------- /lua/mediaplayer/init.lua: -------------------------------------------------------------------------------- 1 | if MediaPlayer then 2 | -- TODO: compare versions? 3 | if MediaPlayer.__refresh then 4 | MediaPlayer.__refresh = nil 5 | else 6 | return -- MediaPlayer has already been registered 7 | end 8 | end 9 | 10 | resource.AddSingleFile "materials/mediaplayer/ui/spritesheet2015-10-7.png" 11 | resource.AddFile "resource/fonts/ClearSans-Medium.ttf" 12 | 13 | AddCSLuaFile "controls/dmediaplayerhtml.lua" 14 | AddCSLuaFile "controls/dhtmlcontrols.lua" 15 | AddCSLuaFile "controls/dmediaplayerrequest.lua" 16 | AddCSLuaFile "cl_init.lua" 17 | AddCSLuaFile "cl_requests.lua" 18 | AddCSLuaFile "cl_idlescreen.lua" 19 | AddCSLuaFile "cl_screen.lua" 20 | AddCSLuaFile "shared.lua" 21 | AddCSLuaFile "sh_events.lua" 22 | AddCSLuaFile "sh_mediaplayer.lua" 23 | AddCSLuaFile "sh_services.lua" 24 | AddCSLuaFile "sh_history.lua" 25 | AddCSLuaFile "sh_metadata.lua" 26 | AddCSLuaFile "sh_cvars.lua" 27 | 28 | include "shared.lua" 29 | include "sv_requests.lua" 30 | 31 | -- TODO: move this into its own file 32 | MediaPlayer.net = MediaPlayer.net or {} 33 | 34 | function MediaPlayer.net.ReadMediaPlayer() 35 | 36 | local mpId = net.ReadString() 37 | local mp = MediaPlayer.GetById(mpId) 38 | 39 | if not IsValid(mp) then 40 | if MediaPlayer.DEBUG then 41 | print("MEDIAPLAYER.Request: Invalid media player ID", mpId, mp) 42 | end 43 | return false 44 | end 45 | 46 | return mp 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/base/cl_draw.lua: -------------------------------------------------------------------------------- 1 | local pcall = pcall 2 | local Color = Color 3 | local RealTime = RealTime 4 | local ValidPanel = ValidPanel 5 | local Vector = Vector 6 | local cam = cam 7 | local draw = draw 8 | local math = math 9 | local string = string 10 | local surface = surface 11 | 12 | local DrawHTMLPanel = MediaPlayerUtils.DrawHTMLPanel 13 | local FormatSeconds = MediaPlayerUtils.FormatSeconds 14 | 15 | local TEXT_ALIGN_CENTER = draw.TEXT_ALIGN_CENTER 16 | local TEXT_ALIGN_TOP = draw.TEXT_ALIGN_TOP 17 | local TEXT_ALIGN_BOTTOM = draw.TEXT_ALIGN_BOTTOM 18 | local TEXT_ALIGN_LEFT = draw.TEXT_ALIGN_LEFT 19 | local TEXT_ALIGN_RIGHT = draw.TEXT_ALIGN_RIGHT 20 | 21 | local TextPaddingX = 12 22 | local TextPaddingY = 12 23 | 24 | local TextBoxPaddingX = 8 25 | local TextBoxPaddingY = 2 26 | 27 | local TextBgColor = Color(0, 0, 0, 200) 28 | local BarBgColor = Color(0, 0, 0, 200) 29 | local BarFgColor = Color(255, 255, 255, 255) 30 | 31 | local function DrawText( text, font, x, y, xalign, yalign ) 32 | return draw.SimpleText( text, font, x, y, color_white, xalign, yalign ) 33 | end 34 | 35 | local function DrawTextBox( text, font, x, y, xalign, yalign ) 36 | 37 | xalign = xalign or TEXT_ALIGN_LEFT 38 | yalign = yalign or TEXT_ALIGN_TOP 39 | 40 | surface.SetFont( font ) 41 | tw, th = surface.GetTextSize( text ) 42 | 43 | if xalign == TEXT_ALIGN_CENTER then 44 | x = x - tw/2 45 | elseif xalign == TEXT_ALIGN_RIGHT then 46 | x = x - tw 47 | end 48 | 49 | if yalign == TEXT_ALIGN_CENTER then 50 | y = y - th/2 51 | elseif yalign == TEXT_ALIGN_BOTTOM then 52 | y = y - th 53 | end 54 | 55 | surface.SetDrawColor( TextBgColor ) 56 | surface.DrawRect( x, y, 57 | tw + TextBoxPaddingX * 2, 58 | th + TextBoxPaddingY * 2 ) 59 | 60 | end 61 | 62 | local UTF8SubLastCharPattern = "[^\128-\191][\128-\191]*$" 63 | local OverflowString = "..." -- ellipsis 64 | 65 | --- 66 | -- Limits a rendered string's width based on a maximum width. 67 | -- 68 | -- @param text Text string. 69 | -- @param font Font. 70 | -- @param w Maximum width. 71 | -- @return String String fitting the maximum required width. 72 | -- 73 | local function RestrictStringWidth( text, font, w ) 74 | 75 | -- TODO: Cache this 76 | 77 | surface.SetFont( font ) 78 | local curwidth = surface.GetTextSize( text ) 79 | local overflow = false 80 | 81 | -- Reduce text by one character until it fits 82 | while curwidth > w do 83 | 84 | -- Text has overflowed, append overflow string on return 85 | if not overflow then 86 | overflow = true 87 | end 88 | 89 | -- Cut off last character 90 | text = string.gsub(text, UTF8SubLastCharPattern, "") 91 | 92 | -- Check size again 93 | curwidth = surface.GetTextSize( text .. OverflowString ) 94 | 95 | end 96 | 97 | return overflow and (text .. OverflowString) or text 98 | 99 | end 100 | 101 | function MEDIAPLAYER:DrawHTML( browser, w, h ) 102 | surface.SetDrawColor( 0, 0, 0, 255 ) 103 | surface.DrawRect( 0, 0, w, h ) 104 | DrawHTMLPanel( browser, w, h ) 105 | end 106 | 107 | function MEDIAPLAYER:DrawMediaInfo( media, w, h ) 108 | 109 | -- TODO: Fadeout media info instead of just hiding 110 | if not vgui.CursorVisible() and RealTime() - self._LastMediaUpdate > 3 then 111 | return 112 | end 113 | 114 | -- Text dimensions 115 | local tw, th 116 | 117 | -- Title background 118 | local titleStr = RestrictStringWidth( media:Title(), "MediaTitle", 119 | w - (TextPaddingX * 2 + TextBoxPaddingX * 2) ) 120 | 121 | DrawTextBox( titleStr, "MediaTitle", TextPaddingX, TextPaddingY ) 122 | 123 | -- Title 124 | DrawText( titleStr, "MediaTitle", 125 | TextPaddingX + TextBoxPaddingX, 126 | TextPaddingY + TextBoxPaddingY ) 127 | 128 | -- Track bar 129 | if media:IsTimed() then 130 | 131 | local duration = media:Duration() 132 | local curTime = media:CurrentTime() 133 | local percent = math.Clamp( curTime / duration, 0, 1 ) 134 | 135 | -- Bar height 136 | local bh = math.Round(h * 1/32) 137 | 138 | -- Bar background 139 | draw.RoundedBox( 0, 0, h - bh, w, bh, BarBgColor ) 140 | 141 | -- Bar foreground (progress) 142 | draw.RoundedBox( 0, 0, h - bh, w * percent, bh, BarFgColor ) 143 | 144 | local timeY = h - bh - TextPaddingY * 2 145 | 146 | -- Current time 147 | local curTimeStr = FormatSeconds(math.Clamp(math.Round(curTime), 0, duration)) 148 | 149 | DrawTextBox( curTimeStr, "MediaTitle", TextPaddingX, timeY, 150 | TEXT_ALIGN_LEFT, TEXT_ALIGN_BOTTOM ) 151 | DrawText( curTimeStr, "MediaTitle", TextPaddingX * 2, timeY, 152 | TEXT_ALIGN_LEFT, TEXT_ALIGN_BOTTOM ) 153 | 154 | -- Duration 155 | local durationStr = FormatSeconds( duration ) 156 | 157 | DrawTextBox( durationStr, "MediaTitle", w - TextPaddingX * 2, timeY, 158 | TEXT_ALIGN_RIGHT, TEXT_ALIGN_BOTTOM ) 159 | DrawText( durationStr, "MediaTitle", w - TextBoxPaddingX * 2, timeY, 160 | TEXT_ALIGN_RIGHT, TEXT_ALIGN_BOTTOM ) 161 | 162 | end 163 | 164 | -- Volume 165 | local volume = MediaPlayer.Volume() 166 | local volumeStr = tostring( math.Round( volume * 100 ) ) 167 | 168 | -- DrawText( volumeStr, "MediaTitle", w - TextPaddingX, h/2, 169 | -- TEXT_ALIGN_CENTER ) 170 | 171 | 172 | -- Loading indicator 173 | 174 | end 175 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/base/cl_fullscreen.lua: -------------------------------------------------------------------------------- 1 | local pcall = pcall 2 | local Color = Color 3 | local RealTime = RealTime 4 | local ScrW = ScrW 5 | local ScrH = ScrH 6 | local ValidPanel = ValidPanel 7 | local Vector = Vector 8 | local cam = cam 9 | local draw = draw 10 | local math = math 11 | local string = string 12 | local surface = surface 13 | 14 | local FullscreenCvar = MediaPlayer.Cvars.Fullscreen 15 | 16 | --[[--------------------------------------------------------- 17 | Convar callback 18 | -----------------------------------------------------------]] 19 | 20 | local function OnFullscreenConVarChanged( name, old, new ) 21 | 22 | new = (new == "1.00") 23 | old = (old == "1.00") 24 | 25 | local media 26 | 27 | for _, mp in pairs(MediaPlayer.List) do 28 | 29 | mp._LastMediaUpdate = RealTime() 30 | 31 | media = mp:CurrentMedia() 32 | 33 | if IsValid(media) and ValidPanel(media.Browser) then 34 | MediaPlayer.SetBrowserSize( media.Browser ) 35 | end 36 | 37 | end 38 | 39 | MediaPlayer.SetBrowserSize( MediaPlayer.GetIdlescreen() ) 40 | 41 | hook.Run( "MediaPlayerFullscreenToggled", new, old ) 42 | 43 | end 44 | cvars.AddChangeCallback( FullscreenCvar:GetName(), OnFullscreenConVarChanged ) 45 | 46 | 47 | --[[--------------------------------------------------------- 48 | Client controls for toggling fullscreen 49 | -----------------------------------------------------------]] 50 | 51 | inputhook.AddKeyPress( KEY_F11, "Toggle MediaPlayer Fullscreen", function() 52 | 53 | local isFullscreen = FullscreenCvar:GetBool() 54 | local numMp = #MediaPlayer.GetAll() 55 | 56 | -- only toggle if there's an active media player or we're in fullscreen mode 57 | if numMp == 0 and not isFullscreen then 58 | return 59 | end 60 | 61 | local value = isFullscreen and 0 or 1 62 | RunConsoleCommand( "mediaplayer_fullscreen", value ) 63 | 64 | end ) 65 | 66 | 67 | --[[--------------------------------------------------------- 68 | Draw functions 69 | -----------------------------------------------------------]] 70 | 71 | function MEDIAPLAYER:DrawFullscreen() 72 | 73 | -- Don't draw if we're not fullscreen 74 | if not FullscreenCvar:GetBool() then return end 75 | 76 | local w, h = ScrW(), ScrH() 77 | local media = self:CurrentMedia() 78 | 79 | if IsValid(media) then 80 | 81 | -- Custom media draw function 82 | if media.Draw then 83 | media:Draw( w, h ) 84 | end 85 | -- TODO: else draw 'not yet implemented' screen? 86 | 87 | -- Draw media info 88 | local succ, err = pcall( self.DrawMediaInfo, self, media, w, h ) 89 | if not succ then 90 | print( err ) 91 | end 92 | 93 | else 94 | 95 | local browser = MediaPlayer.GetIdlescreen() 96 | 97 | if ValidPanel(browser) then 98 | self:DrawHTML( browser, w, h ) 99 | end 100 | 101 | end 102 | 103 | end 104 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/base/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | include "cl_draw.lua" 3 | include "cl_fullscreen.lua" 4 | include "net.lua" 5 | 6 | local CeilPower2 = MediaPlayerUtils.CeilPower2 7 | 8 | function MEDIAPLAYER:NetReadUpdate() 9 | -- Allows for another media player type to extend update net messages 10 | end 11 | 12 | function MEDIAPLAYER:OnNetReadMedia( media ) 13 | -- Allows for another media player type to extend media net messages 14 | end 15 | 16 | function MEDIAPLAYER:OnQueueKeyPressed( down, held ) 17 | self._LastMediaUpdate = RealTime() 18 | end 19 | 20 | 21 | --[[--------------------------------------------------------- 22 | Networking 23 | -----------------------------------------------------------]] 24 | 25 | local function OnMediaUpdate( len ) 26 | 27 | local mpId = net.ReadString() 28 | local mpType = net.ReadString() 29 | 30 | if MediaPlayer.DEBUG then 31 | print( "Received MEDIAPLAYER.Update", mpId, mpType ) 32 | end 33 | 34 | local mp = MediaPlayer.GetById(mpId) 35 | if not mp then 36 | mp = MediaPlayer.Create( mpId, mpType ) 37 | end 38 | 39 | -- Read owner; may be NULL 40 | local owner = net.ReadEntity() 41 | if IsValid( owner ) then 42 | mp:SetOwner( owner ) 43 | end 44 | 45 | local state = mp.net.ReadPlayerState() 46 | 47 | local queueRepeat = net.ReadBool() 48 | mp:SetQueueRepeat( queueRepeat ) 49 | 50 | local queueShuffle = net.ReadBool() 51 | mp:SetQueueShuffle( queueShuffle ) 52 | 53 | local queueLocked = net.ReadBool() 54 | mp:SetQueueLocked( queueLocked ) 55 | 56 | -- Read extended update information 57 | mp:NetReadUpdate() 58 | 59 | -- Clear old queue 60 | mp:ClearMediaQueue() 61 | 62 | -- Read queue information 63 | local count = net.ReadUInt( mp:GetQueueLimit(true) ) 64 | for i = 1, count do 65 | local media = mp.net.ReadMedia() 66 | mp:OnNetReadMedia(media) 67 | mp:AddMedia(media) 68 | end 69 | 70 | mp:QueueUpdated() 71 | 72 | mp:SetPlayerState( state ) 73 | 74 | hook.Run( "OnMediaPlayerUpdate", mp ) 75 | 76 | end 77 | net.Receive( "MEDIAPLAYER.Update", OnMediaUpdate ) 78 | 79 | local function OnMediaSet( len ) 80 | 81 | if MediaPlayer.DEBUG then 82 | print( "Received MEDIAPLAYER.Media" ) 83 | end 84 | 85 | local mpId = net.ReadString() 86 | local mp = MediaPlayer.GetById(mpId) 87 | 88 | if not mp then 89 | if MediaPlayer.DEBUG then 90 | ErrorNoHalt("Received media for invalid mediaplayer\n") 91 | print("ID: " .. tostring(mpId)) 92 | debug.Trace() 93 | end 94 | return 95 | end 96 | 97 | if mp:GetPlayerState() >= MP_STATE_PLAYING then 98 | mp:OnMediaFinished() 99 | mp:QueueUpdated() 100 | end 101 | 102 | local media = mp.net.ReadMedia() 103 | 104 | if media then 105 | local startTime = mp.net.ReadTime() 106 | media:StartTime( startTime ) 107 | 108 | mp:OnNetReadMedia(media) 109 | 110 | local state = mp:GetPlayerState() 111 | 112 | if state == MP_STATE_PLAYING then 113 | media:Play() 114 | else 115 | media:Pause() 116 | end 117 | end 118 | 119 | mp:SetMedia( media ) 120 | 121 | end 122 | net.Receive( "MEDIAPLAYER.Media", OnMediaSet ) 123 | 124 | local function OnMediaRemoved( len ) 125 | 126 | if MediaPlayer.DEBUG then 127 | print( "Received MEDIAPLAYER.Remove" ) 128 | end 129 | 130 | local mpId = net.ReadString() 131 | local mp = MediaPlayer.GetById(mpId) 132 | if not mp then return end 133 | 134 | mp:Remove() 135 | 136 | end 137 | net.Receive( "MEDIAPLAYER.Remove", OnMediaRemoved ) 138 | 139 | local function OnMediaSeek( len ) 140 | 141 | local mpId = net.ReadString() 142 | local mp = MediaPlayer.GetById(mpId) 143 | if not ( mp and (mp:GetPlayerState() >= MP_STATE_PLAYING) ) then return end 144 | 145 | local startTime = mp.net.ReadTime() 146 | 147 | if MediaPlayer.DEBUG then 148 | print( "Received MEDIAPLAYER.Seek", mpId, startTime ) 149 | end 150 | 151 | local media = mp:CurrentMedia() 152 | 153 | if media then 154 | media:StartTime( startTime ) 155 | else 156 | ErrorNoHalt('ERROR: MediaPlayer received seek message while no media is playing' .. 157 | '[' .. mpId .. ']\n') 158 | MediaPlayer.RequestUpdate( mp ) 159 | end 160 | 161 | end 162 | net.Receive( "MEDIAPLAYER.Seek", OnMediaSeek ) 163 | 164 | local function OnMediaPause( len ) 165 | 166 | local mpId = net.ReadString() 167 | local mp = MediaPlayer.GetById(mpId) 168 | if not mp then return end 169 | 170 | local state = mp.net.ReadPlayerState() 171 | 172 | if MediaPlayer.DEBUG then 173 | print( "Received MEDIAPLAYER.Pause", mpId, state ) 174 | end 175 | 176 | mp:SetPlayerState( state ) 177 | 178 | end 179 | net.Receive( "MEDIAPLAYER.Pause", OnMediaPause ) 180 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/base/net.lua: -------------------------------------------------------------------------------- 1 | local net = net 2 | local CeilPower2 = MediaPlayerUtils.CeilPower2 3 | 4 | local EOT = "\4" -- End of transmission 5 | 6 | MEDIAPLAYER.net = {} 7 | local mpnet = MEDIAPLAYER.net 8 | 9 | function mpnet.ReadDuration() 10 | return net.ReadUInt(16) 11 | end 12 | 13 | function mpnet.WriteDuration( seconds ) 14 | net.WriteUInt( seconds, 16 ) 15 | end 16 | 17 | function mpnet.ReadMedia() 18 | local uid = net.ReadString() 19 | 20 | if uid == EOT then 21 | return nil 22 | end 23 | 24 | local url = net.ReadString() 25 | local metadata = net.ReadTable() 26 | local ownerName = net.ReadString() 27 | local ownerSteamId = net.ReadString() 28 | 29 | -- Create media object 30 | local media = MediaPlayer.GetMediaForUrl( url, true ) 31 | 32 | -- Set uniqud ID to match the server 33 | media._id = uid 34 | 35 | media:SetMetadata( metadata, true ) 36 | media._OwnerName = ownerName 37 | media._OwnerSteamID = ownerSteamId 38 | 39 | return media 40 | end 41 | 42 | function mpnet.WriteMedia( media ) 43 | if media then 44 | net.WriteString( media:UniqueID() ) 45 | net.WriteString( media:Url() ) 46 | net.WriteTable( media._metadata or {} ) 47 | net.WriteString( media:OwnerName() ) 48 | net.WriteString( media:OwnerSteamID() ) 49 | else 50 | net.WriteString( EOT ) 51 | end 52 | end 53 | 54 | local StateBits = CeilPower2(NUM_MP_STATE) / 2 55 | 56 | function mpnet.ReadPlayerState() 57 | return net.ReadUInt(StateBits) 58 | end 59 | 60 | function mpnet.WritePlayerState( state ) 61 | net.WriteUInt(state, StateBits) 62 | end 63 | 64 | --- 65 | -- Threshold for determining if server and client system time differ. 66 | -- 67 | local TIME_OFFSET_THRESHOLD = 2 68 | 69 | --- 70 | -- Adjusts time returned from the server since RealTime will always differ. 71 | -- 72 | local function correctTime( time, serverTime ) 73 | local curTime = RealTime() 74 | local diffTime = os.difftime( serverTime, curTime ) 75 | 76 | if math.abs(diffTime) > TIME_OFFSET_THRESHOLD then 77 | return time - diffTime 78 | else 79 | return time 80 | end 81 | end 82 | 83 | function mpnet.ReadTime() 84 | local time = net.ReadInt(32) 85 | local sync = net.ReadBit() == 1 86 | 87 | if sync then 88 | local serverTime = net.ReadInt(32) 89 | return correctTime(time, serverTime) 90 | else 91 | return time 92 | end 93 | end 94 | 95 | --- 96 | -- Writes the given epoch. 97 | -- 98 | -- @param time Epoch. 99 | -- @param sync Whether the time should be synced on the client (default: true). 100 | -- 101 | function mpnet.WriteTime( time, sync ) 102 | if sync == nil then sync = true end 103 | sync = tobool(sync) 104 | 105 | net.WriteInt( time, 32 ) 106 | net.WriteBit( sync ) 107 | 108 | if sync then 109 | -- We must send the current time in case either the server or the 110 | -- client's system clock is offset. 111 | net.WriteInt( RealTime(), 32 ) 112 | end 113 | end 114 | 115 | --- 116 | -- Read a vote value or count. 117 | -- 118 | function mpnet.ReadVote() 119 | return net.ReadInt(9) 120 | end 121 | 122 | --- 123 | -- Write a vote value or count. 124 | -- 125 | function mpnet.WriteVote( value ) 126 | net.WriteInt( value, 9 ) 127 | end 128 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/base/sh_snapshot.lua: -------------------------------------------------------------------------------- 1 | function MEDIAPLAYER:GetSnapshot() 2 | local queue = table.Copy( self:GetMediaQueue() ) 3 | local media = self:GetMedia() 4 | 5 | return { 6 | media = media, 7 | currentTime = media and media:CurrentTime(), 8 | queue = queue, 9 | queueRepeat = self:GetQueueRepeat(), 10 | queueShuffle = self:GetQueueShuffle(), 11 | queueLocked = self:GetQueueLocked() 12 | } 13 | end 14 | 15 | function MEDIAPLAYER:RestoreSnapshot( snapshot ) 16 | self._Queue = {} 17 | 18 | self:SetQueueRepeat( snapshot.queueRepeat ) 19 | self:SetQueueShuffle( snapshot.queueShuffle ) 20 | self:SetQueueLocked( snapshot.queueLocked ) 21 | 22 | if snapshot.media then 23 | -- restore currently playing media from where it left off 24 | local mediaSnapshot = snapshot.media 25 | local media = MediaPlayer.GetMediaForUrl( mediaSnapshot.url ) 26 | if media then 27 | table.Merge( media, mediaSnapshot ) 28 | media:StartTime( RealTime() - snapshot.currentTime ) 29 | self:SetMedia( media ) 30 | end 31 | else 32 | self:SetMedia( nil ) 33 | end 34 | 35 | if snapshot.queue then 36 | -- restore queue 37 | for _, mediaSnapshot in ipairs( snapshot.queue ) do 38 | local media = MediaPlayer.GetMediaForUrl( mediaSnapshot.url ) 39 | if media then 40 | table.Merge( media, mediaSnapshot ) 41 | self:AddMedia( media ) 42 | end 43 | end 44 | 45 | self:QueueUpdated() 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/components/vote.lua: -------------------------------------------------------------------------------- 1 | --[[-------------------------------------------- 2 | Vote object 3 | ----------------------------------------------]] 4 | 5 | local VOTE = {} 6 | VOTE.__index = VOTE 7 | 8 | function VOTE:New( ply, value ) 9 | local obj = setmetatable( {}, self ) 10 | 11 | obj.player = ply 12 | obj.value = value or 1 13 | 14 | return obj 15 | end 16 | 17 | function VOTE:IsValid() 18 | return IsValid(self.player) 19 | end 20 | 21 | function VOTE:GetPlayer() 22 | return self.player 23 | end 24 | 25 | function VOTE:GetValue() 26 | return self.value 27 | end 28 | 29 | MediaPlayer.VOTE = VOTE 30 | 31 | 32 | --[[-------------------------------------------- 33 | Vote Manager 34 | ----------------------------------------------]] 35 | 36 | local VoteManager = {} 37 | VoteManager.__index = VoteManager 38 | 39 | -- 40 | -- Initialize the media player object. 41 | -- 42 | -- @param mp Media player object. 43 | -- 44 | function VoteManager:New( mp ) 45 | local obj = setmetatable({}, self) 46 | obj._mp = mp 47 | obj._votes = {} 48 | return obj 49 | end 50 | 51 | --- 52 | -- Clears all votes. 53 | -- 54 | function VoteManager:Clear() 55 | self._votes = {} 56 | end 57 | 58 | function VoteManager:ClearVotesForMedia( media ) 59 | self._votes[media:UniqueID()] = nil 60 | end 61 | 62 | --- 63 | -- Add vote for a player and media 64 | -- 65 | -- @param media Media object. 66 | -- @param ply Player. 67 | -- @param value Vote value. 68 | -- 69 | function VoteManager:AddVote( media, ply, value ) 70 | if not IsValid(ply) then return end 71 | 72 | local uid = media:UniqueID() 73 | 74 | local votes 75 | 76 | if self._votes[uid] then 77 | votes = self._votes[uid] 78 | else 79 | votes = { 80 | media = media, 81 | count = 0 82 | } 83 | self._votes[uid] = votes 84 | end 85 | 86 | local vote = self:GetVoteByPlayer(media, ply) 87 | 88 | -- Update vote if player has already voted 89 | if vote then 90 | vote.value = value 91 | else 92 | vote = VOTE:New(ply, value) 93 | table.insert( votes, vote ) 94 | end 95 | 96 | -- player is retracting their vote 97 | if value == 0 then 98 | for k, v in ipairs(votes) do 99 | if v:GetPlayer() == ply then 100 | table.remove( votes, k ) 101 | break 102 | end 103 | end 104 | end 105 | 106 | -- recalculate vote count 107 | self:GetVoteCountForMedia( media, true ) 108 | end 109 | 110 | --- 111 | -- Remove the player's vote for a media item. 112 | -- 113 | -- @param media Media object. 114 | -- @param ply Player. 115 | -- 116 | function VoteManager:RemoveVote( media, ply ) 117 | self:AddVote( media, ply, 0 ) 118 | end 119 | 120 | --- 121 | -- Get whether the player has already voted for the media. 122 | -- 123 | -- @param media Media object. 124 | -- @param ply Player. 125 | -- @return Whether the player has voted for the media. 126 | -- 127 | function VoteManager:HasVoted( media, ply ) 128 | local uid = media:UniqueID() 129 | 130 | local votes = self._votes[uid] 131 | if not votes then return false end 132 | 133 | for k, vote in ipairs(votes) do 134 | if vote:GetPlayer() == ply then 135 | return true 136 | end 137 | end 138 | 139 | return false 140 | end 141 | 142 | --- 143 | -- Get the vote count for the given media. 144 | -- 145 | -- @param media Media object or UID. 146 | -- @return Vote count for media. 147 | -- 148 | function VoteManager:GetVoteCountForMedia( media, forceCalc ) 149 | local uid = isstring(media) and media or media:UniqueID() 150 | 151 | local votes = self._votes[uid] 152 | if not votes then return 0 end 153 | 154 | if not votes.count or forceCalc then 155 | local count = 0 156 | 157 | for k, vote in ipairs(votes) do 158 | count = count + vote:GetValue() 159 | end 160 | 161 | votes.count = count 162 | end 163 | 164 | return votes.count 165 | end 166 | 167 | function VoteManager:GetVoteByPlayer( media, ply ) 168 | local uid = media:UniqueID() 169 | 170 | local votes = self._votes[uid] 171 | if not votes then return nil end 172 | 173 | for _, vote in ipairs(votes) do 174 | if vote:GetPlayer() == ply then 175 | return vote 176 | end 177 | end 178 | 179 | return nil 180 | end 181 | 182 | --- 183 | -- Get the top voted media unique ID. VoteManager:Invalidate() should be called 184 | -- prior to this in case any players may have disconnected. 185 | -- 186 | -- @param removeMedia Remove the media from the vote manager. 187 | -- @return Top voted media UID. 188 | -- 189 | function VoteManager:GetTopVote( removeMedia ) 190 | local media, topVotes = nil, nil 191 | 192 | for uid, _ in pairs(self._votes) do 193 | local votes = self:GetVoteCountForMedia( uid ) 194 | 195 | if not topVotes or votes > topVotes then 196 | media = self._votes[uid].media 197 | topVotes = votes 198 | end 199 | end 200 | 201 | if removeMedia and media then 202 | self._votes[media:UniqueID()] = nil 203 | end 204 | 205 | return media, topVotes 206 | end 207 | 208 | --- 209 | -- Iterate through all votes and determine if they're still valid. This should 210 | -- called prior to getting the top vote. 211 | -- 212 | -- @return Whether any votes were invalid and removed. 213 | -- 214 | function VoteManager:Invalidate() 215 | local changed = false 216 | 217 | for uid, votes in pairs(self._votes) do 218 | local numVotes = 0 219 | 220 | for k, vote in ipairs(votes) do 221 | -- check for valid player in case they may have disconnected 222 | if not (IsValid(vote) and self._mp:HasListener(vote:GetPlayer())) then 223 | table.remove( votes, k ) 224 | changed = true 225 | else 226 | numVotes = numVotes + 1 227 | end 228 | end 229 | 230 | if numVotes == 0 then 231 | self._votes[uid] = nil 232 | end 233 | end 234 | 235 | return changed 236 | end 237 | 238 | MediaPlayer.VoteManager = VoteManager 239 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/components/voteskip.lua: -------------------------------------------------------------------------------- 1 | --[[-------------------------------------------- 2 | Voteskip Manager 3 | ----------------------------------------------]] 4 | 5 | local VoteskipManager = {} 6 | VoteskipManager.__index = VoteskipManager 7 | 8 | local VOTESKIP_REQ_VOTE_RATIO = 2/3 9 | 10 | --- 11 | -- Initialize the Voteskip Manager object. 12 | -- 13 | function VoteskipManager:New( mp, ratio ) 14 | local obj = setmetatable({}, self) 15 | obj._mp = mp 16 | obj._votes = {} 17 | obj._value = 0 18 | obj._ratio = VOTESKIP_REQ_VOTE_RATIO 19 | return obj 20 | end 21 | 22 | function VoteskipManager:GetNumVotes() 23 | return self._value 24 | end 25 | 26 | function VoteskipManager:GetNumRequiredVotes( totalPlayers ) 27 | return math.ceil( totalPlayers * self._ratio ) 28 | end 29 | 30 | function VoteskipManager:GetNumRemainingVotes( totalPlayers ) 31 | local numVotes = self:GetNumVotes() 32 | local reqVotes = self:GetNumRequiredVotes( totalPlayers ) 33 | return ( reqVotes - numVotes ) 34 | end 35 | 36 | function VoteskipManager:ShouldSkip( totalPlayers ) 37 | if totalPlayers <= 0 then 38 | return false 39 | end 40 | 41 | self:Invalidate() 42 | 43 | local reqVotes = self:GetNumRequiredVotes( totalPlayers ) 44 | return ( self._value >= reqVotes ) 45 | end 46 | 47 | --- 48 | -- Clears all votes. 49 | -- 50 | function VoteskipManager:Clear() 51 | self._votes = {} 52 | self._value = 0 53 | end 54 | 55 | --- 56 | -- Add vote. 57 | -- 58 | -- @param ply Player. 59 | -- @param value Vote value. 60 | -- 61 | function VoteskipManager:AddVote( ply, value ) 62 | if not IsValid(ply) then return end 63 | if not value then value = 1 end 64 | 65 | -- value can't be negative 66 | value = math.max( 0, value ) 67 | 68 | local uid = ply:SteamID64() 69 | local vote = self._votes[ uid ] 70 | 71 | if vote then 72 | -- update existing vote 73 | if value == 0 then 74 | -- clear player vote 75 | self._votes[ uid ] = nil 76 | else 77 | vote.value = value 78 | end 79 | else 80 | vote = MediaPlayer.VOTE:New( ply, value ) 81 | self._votes[ uid ] = vote 82 | end 83 | 84 | self:Invalidate() 85 | end 86 | 87 | --- 88 | -- Remove the player's vote. 89 | -- 90 | -- @param ply Player. 91 | -- 92 | function VoteskipManager:RemoveVote( ply ) 93 | self:AddVote( ply, 0 ) 94 | end 95 | 96 | --- 97 | -- Get whether the player has already voted. 98 | -- 99 | -- @param ply Player. 100 | -- @return Whether the player has voted. 101 | -- 102 | function VoteskipManager:HasVoted( ply ) 103 | if not IsValid( ply ) then return false end 104 | 105 | local uid = ply:SteamID64() 106 | local vote = self._votes[ uid ] 107 | 108 | return ( vote ~= nil ) 109 | end 110 | 111 | --- 112 | -- Iterate through all votes and determine if they're still valid. This should 113 | -- called prior to getting the top vote. 114 | -- 115 | -- @return Whether any votes were invalid and removed. 116 | -- 117 | function VoteskipManager:Invalidate() 118 | local value = 0 119 | local changed = false 120 | 121 | for uid, vote in pairs(self._votes) do 122 | local ply = vote.player 123 | if IsValid( ply ) and self._mp:HasListener( ply ) then 124 | value = value + vote.value 125 | else 126 | self._votes[ uid ] = nil 127 | changed = true 128 | end 129 | end 130 | 131 | if self._value ~= value then 132 | self._value = value 133 | changed = true 134 | end 135 | 136 | return changed 137 | end 138 | 139 | MediaPlayer.VoteskipManager = VoteskipManager 140 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/entity/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | 3 | DEFINE_BASECLASS( "mp_base" ) 4 | 5 | local pcall = pcall 6 | local print = print 7 | local Angle = Angle 8 | local IsValid = IsValid 9 | local ValidPanel = ValidPanel 10 | local Vector = Vector 11 | local cam = cam 12 | local Start3D = cam.Start3D 13 | local Start3D2D = cam.Start3D2D 14 | local End3D2D = cam.End3D2D 15 | local draw = draw 16 | local math = math 17 | local string = string 18 | local surface = surface 19 | 20 | local FullscreenCvar = MediaPlayer.Cvars.Fullscreen 21 | 22 | MEDIAPLAYER.Enable3DAudio = true 23 | 24 | function MEDIAPLAYER:NetReadUpdate() 25 | local entIndex = net.ReadUInt(16) 26 | local ent = Entity(entIndex) 27 | local mpEnt = self.Entity 28 | 29 | if MediaPlayer.DEBUG then 30 | print("MEDIAPLAYER.NetReadUpdate[entity]: ", ent, entIndex) 31 | end 32 | 33 | if ent ~= mpEnt then 34 | if IsValid(ent) and ent ~= NULL then 35 | ent:InstallMediaPlayer( self ) 36 | else 37 | -- Wait until the entity becomes valid 38 | self._EntIndex = entIndex 39 | end 40 | end 41 | end 42 | 43 | local RenderScale = 0.1 44 | local InfoScale = 1/17 45 | 46 | function MEDIAPLAYER:GetOrientation() 47 | local ent = self.Entity 48 | 49 | if ent then 50 | return ent:GetMediaPlayerPosition() 51 | end 52 | 53 | return nil 54 | end 55 | 56 | --- 57 | -- Draws the idlescreen; this is drawn when there is no media playing. 58 | -- 59 | function MEDIAPLAYER:DrawIdlescreen( w, h ) 60 | local browser = MediaPlayer.GetIdlescreen() 61 | 62 | if ValidPanel(browser) then 63 | self:DrawHTML( browser, w, h ) 64 | end 65 | end 66 | 67 | local BaseInfoHeight = 60 68 | 69 | function MEDIAPLAYER:Draw( bDrawingDepth, bDrawingSkybox ) 70 | 71 | local ent = self.Entity 72 | 73 | if --bDrawingSkybox or 74 | FullscreenCvar:GetBool() or -- Don't draw if we're drawing fullscreen 75 | not IsValid(ent) or 76 | (ent.IsDormant and ent:IsDormant()) then 77 | return 78 | end 79 | 80 | local media = self:GetMedia() 81 | local w, h, pos, ang = self:GetOrientation() 82 | 83 | -- Render scale 84 | local rw, rh = w / RenderScale, h / RenderScale 85 | 86 | if IsValid(media) then 87 | 88 | -- Custom media draw function 89 | if media.Draw then 90 | Start3D2D( pos, ang, RenderScale ) 91 | media:Draw( rw, rh ) 92 | End3D2D() 93 | end 94 | -- TODO: else draw 'not yet implemented' screen? 95 | 96 | -- scale based off of height 97 | local scale = InfoScale * ( h / BaseInfoHeight ) 98 | 99 | -- Media info 100 | Start3D2D( pos, ang, scale ) 101 | local iw, ih = w / scale, h / scale 102 | self:DrawMediaInfo( media, iw, ih ) 103 | End3D2D() 104 | 105 | else 106 | 107 | Start3D2D( pos, ang, RenderScale ) 108 | self:DrawIdlescreen( rw, rh ) 109 | End3D2D() 110 | 111 | end 112 | 113 | end 114 | 115 | function MEDIAPLAYER:SetMedia( media ) 116 | if media then 117 | -- Set entity on media for 3D audio support and setting proper 118 | -- browser resolution 119 | media.Entity = self:GetEntity() 120 | end 121 | 122 | BaseClass.SetMedia( self, media ) 123 | end 124 | 125 | --- 126 | -- Mouse click intersected with 3D2D screen. 127 | -- 128 | function MEDIAPLAYER:OnMousePressed( x, y ) 129 | local media = self:GetMedia() 130 | if media and media:IsMouseInputEnabled() then 131 | media:OnMousePressed( x, y ) 132 | end 133 | end 134 | 135 | function MEDIAPLAYER:OnMouseWheeled( scrollDelta ) 136 | local media = self:GetMedia() 137 | if media and media:IsMouseInputEnabled() then 138 | media:OnMouseWheeled( scrollDelta ) 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/entity/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "shared.lua" 2 | AddCSLuaFile "sh_meta.lua" 3 | include "shared.lua" 4 | 5 | DEFINE_BASECLASS( "mp_base" ) 6 | 7 | function MEDIAPLAYER:NetWriteUpdate() 8 | -- Write the entity index since the actual entity may not yet exist on a 9 | -- client that's not fully connected. 10 | local entIndex = IsValid(self.Entity) and self.Entity:EntIndex() or 0 11 | net.WriteUInt(entIndex, 16) 12 | end 13 | 14 | function MEDIAPLAYER:NextMedia() 15 | 16 | BaseClass.NextMedia( self ) 17 | 18 | if IsValid(self.Entity) then 19 | local media = self:GetMedia() 20 | 21 | -- Fire outputs on the entity which can be used by mappers to create 22 | -- effects such as lights turning on/off 23 | if media then 24 | self.Entity:Fire( "OnMediaStarted", nil, 0 ) 25 | else 26 | self.Entity:Fire( "OnQueueEmpty", nil, 0 ) 27 | end 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/entity/sh_meta.lua: -------------------------------------------------------------------------------- 1 | --[[--------------------------------------------------------- 2 | Media Player Entity Meta 3 | -----------------------------------------------------------]] 4 | 5 | local EntityMeta = FindMetaTable("Entity") 6 | if not EntityMeta then return end 7 | 8 | function EntityMeta:GetMediaPlayer() 9 | return self._mp 10 | end 11 | 12 | -- 13 | -- Installs a media player reference to the entity. 14 | -- 15 | -- @param Table|String? mp Media player table or string type. 16 | -- @param String? mpId Media player unique ID. 17 | -- 18 | function EntityMeta:InstallMediaPlayer( mp, mpId ) 19 | if not istable(mp) then 20 | local mpType = isstring(mp) and mp or "entity" 21 | 22 | if not MediaPlayer.IsValidType(mpType) then 23 | ErrorNoHalt("ERROR: Attempted to install invalid mediaplayer type onto an entity!\n") 24 | ErrorNoHalt("ENTITY: " .. tostring(self) .. "\n") 25 | ErrorNoHalt("TYPE: " .. tostring(mpType) .. "\n") 26 | mpType = "entity" -- default 27 | end 28 | 29 | local mpId = mpId or "Entity" .. self:EntIndex() 30 | mp = MediaPlayer.Create( mpId, mpType ) 31 | end 32 | 33 | self._mp = mp 34 | self._mp:SetEntity(self) 35 | 36 | local creator = self.GetCreator and self:GetCreator() 37 | if IsValid( creator ) then 38 | self._mp:SetOwner( creator ) 39 | end 40 | 41 | if isfunction(self.SetupMediaPlayer) then 42 | self:SetupMediaPlayer(mp) 43 | end 44 | 45 | return mp 46 | end 47 | 48 | local DefaultConfig = { 49 | offset = Vector(0,0,0), -- translation from entity origin 50 | angle = Angle(0,90,90), -- rotation 51 | -- attachment = "corner" -- attachment name 52 | width = 64, -- screen width 53 | height = 64 * 9/16 -- screen height 54 | } 55 | 56 | function EntityMeta:GetMediaPlayerPosition() 57 | local cfg = self.PlayerConfig or DefaultConfig 58 | 59 | local w = (cfg.width or DefaultConfig.width) 60 | local h = (cfg.height or DefaultConfig.height) 61 | local angles = (cfg.angle or DefaultConfig.angle) 62 | 63 | local pos, ang 64 | 65 | if cfg.attachment then 66 | local idx = self:LookupAttachment(cfg.attachment) 67 | if not idx then 68 | local err = string.format("MediaPlayer:Entity.Draw: Invalid attachment '%s'\n", cfg.attachment) 69 | Error(err) 70 | end 71 | 72 | -- Get attachment orientation 73 | local attach = self:GetAttachment(idx) 74 | pos = attach.pos 75 | ang = attach.ang 76 | else 77 | pos = self:GetPos() -- TODO: use GetRenderOrigin? 78 | end 79 | 80 | -- Apply offset 81 | if cfg.offset then 82 | pos = pos + 83 | self:GetForward() * cfg.offset.x + 84 | self:GetRight() * cfg.offset.y + 85 | self:GetUp() * cfg.offset.z 86 | end 87 | 88 | -- Set angles 89 | ang = ang or self:GetAngles() -- TODO: use GetRenderAngles? 90 | 91 | ang:RotateAroundAxis( ang:Right(), angles.p ) 92 | ang:RotateAroundAxis( ang:Up(), angles.y ) 93 | ang:RotateAroundAxis( ang:Forward(), angles.r ) 94 | 95 | return w, h, pos, ang 96 | end 97 | -------------------------------------------------------------------------------- /lua/mediaplayer/players/entity/shared.lua: -------------------------------------------------------------------------------- 1 | include "sh_meta.lua" 2 | 3 | DEFINE_BASECLASS( "mp_base" ) 4 | 5 | --[[--------------------------------------------------------- 6 | Entity Media Player 7 | -----------------------------------------------------------]] 8 | 9 | local MEDIAPLAYER = MEDIAPLAYER 10 | MEDIAPLAYER.Name = "entity" 11 | 12 | function MEDIAPLAYER:IsValid() 13 | if not BaseClass.IsValid(self) then 14 | return false 15 | end 16 | 17 | local ent = self.Entity 18 | 19 | if ent then 20 | return IsValid(ent) 21 | end 22 | 23 | -- Client may still be waiting on the entity to be created by the network; 24 | -- let's just say it's valid until the entity is setup 25 | return true 26 | end 27 | 28 | function MEDIAPLAYER:Init(...) 29 | BaseClass.Init(self, ...) 30 | 31 | if SERVER then 32 | -- Manually manage listeners by default 33 | self._TransmitState = TRANSMIT_NEVER 34 | end 35 | end 36 | 37 | function MEDIAPLAYER:SetEntity(ent) 38 | self.Entity = ent 39 | 40 | if SERVER then 41 | local creator = ent:GetCreator() 42 | 43 | if IsValid(creator) and creator:IsPlayer() then 44 | self:SetOwner(creator) 45 | end 46 | else 47 | -- Setup hooks for drawing the screen onto the entity 48 | hook.Add( "HUDPaint", self, self.DrawFullscreen ) 49 | hook.Add( "PostDrawOpaqueRenderables", self, self.Draw ) 50 | end 51 | end 52 | 53 | function MEDIAPLAYER:GetEntity() 54 | -- Clients may wait for the entity to become valid 55 | if CLIENT and self._EntIndex then 56 | local ent = Entity(self._EntIndex) 57 | 58 | if IsValid(ent) and ent ~= NULL then 59 | ent:InstallMediaPlayer(self) 60 | self._EntIndex = nil 61 | else 62 | return nil 63 | end 64 | end 65 | 66 | return self.Entity 67 | end 68 | 69 | function MEDIAPLAYER:GetPos() 70 | return IsValid(self.Entity) and self.Entity:GetPos() or Vector(0,0,0) 71 | end 72 | 73 | function MEDIAPLAYER:GetLocation() 74 | if IsValid(self.Entity) and self.Entity.Location then 75 | return self.Entity:Location() 76 | end 77 | return self._Location 78 | end 79 | 80 | function MEDIAPLAYER:Think() 81 | BaseClass.Think(self) 82 | 83 | local ent = self:GetEntity() 84 | 85 | if IsValid(ent) then 86 | -- Lua refresh fix 87 | if ent._mp ~= self then 88 | self:Remove() 89 | end 90 | elseif SERVER then 91 | -- Only remove on the server since the client may still be connecting 92 | -- and the entity will be created when they finish. 93 | self:Remove() 94 | end 95 | end 96 | 97 | function MEDIAPLAYER:Remove() 98 | -- remove draw hooks 99 | if CLIENT then 100 | hook.Remove( "HUDPaint", self ) 101 | hook.Remove( "PostDrawOpaqueRenderables", self ) 102 | end 103 | 104 | -- remove reference to media player installed on entity 105 | if self.Entity then 106 | self.Entity._mp = nil 107 | end 108 | 109 | BaseClass.Remove(self) 110 | end 111 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/audiofile/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "shared.lua" 2 | include "shared.lua" 3 | 4 | local urllib = url 5 | local FilenamePattern = "([^/]+)%.%w-$" 6 | 7 | local function titleFallback(self, callback) 8 | local path = self.urlinfo.path 9 | path = string.match( path, FilenamePattern ) -- get filename 10 | 11 | title = urllib.unescape( path ) 12 | self._metadata.title = title 13 | 14 | self:SetMetadata(self._metadata, true) 15 | MediaPlayer.Metadata:Save(self) 16 | 17 | callback(self._metadata) 18 | end 19 | 20 | --[[local function id3(self, callback) 21 | self:Fetch( self.url, 22 | function(body, len, headers, code) 23 | local title, artist 24 | 25 | -- check header 26 | if body:sub(1, 4) == "TAG+" then 27 | title = body:sub(5, 56) 28 | artist = body:sub(57, 116) 29 | elseif body:sub(1, 3) == "TAG" then 30 | title = body:sub(4, 33) 31 | artist = body:sub(34, 63) 32 | else 33 | titleFallback(self, callback) 34 | return 35 | end 36 | 37 | title = title:Trim() 38 | artist = artist:Trim() 39 | 40 | print("ID3 SUCCESS:", title, artist) 41 | 42 | if artist:len() > 0 then 43 | title = artist .. ' - ' .. title 44 | end 45 | 46 | self._metadata.title = title 47 | 48 | callback(self._metadata) 49 | end, 50 | 51 | function() 52 | titleFallback(self, callback) 53 | end, 54 | 55 | { 56 | ["Range"] = "bytes=-128" 57 | } 58 | ) 59 | end]] 60 | 61 | function SERVICE:GetMetadata( callback ) 62 | 63 | local ext = self:GetExtension() 64 | 65 | -- if ext == 'mp3' then 66 | -- id3(self, callback) 67 | -- else 68 | 69 | if not self._metadata then 70 | self._metadata = { 71 | title = "Unknown audio", 72 | duration = 0 73 | } 74 | end 75 | 76 | if callback then 77 | self:SetMetadata(self._metadata, true) 78 | MediaPlayer.Metadata:Save(self) 79 | 80 | callback(self._metadata) 81 | end 82 | 83 | -- end 84 | 85 | end 86 | 87 | function SERVICE:GetExtension() 88 | if not self._extension then 89 | self._extension = string.GetExtensionFromFilename(self.url) 90 | end 91 | return self._extension 92 | end 93 | 94 | function SERVICE:NetReadRequest() 95 | 96 | if not self.PrefetchMetadata then return end 97 | 98 | local title = net.ReadString() 99 | 100 | -- If the title is just the URL, grab just the filename instead 101 | if title == self.url then 102 | local path = self.urlinfo.path 103 | path = string.match( path, FilenamePattern ) -- get filename 104 | 105 | title = urllib.unescape( path ) 106 | end 107 | 108 | self._metadata = self._metadata or {} 109 | self._metadata.title = title 110 | self._metadata.duration = net.ReadUInt( 16 ) 111 | 112 | end 113 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/audiofile/shared.lua: -------------------------------------------------------------------------------- 1 | local urllib = url 2 | 3 | SERVICE.Name = "Audio file" 4 | SERVICE.Id = "af" 5 | 6 | SERVICE.PrefetchMetadata = true 7 | 8 | local SupportedEncodings = { 9 | '([^/]+%.mp3)', -- mp3 10 | '([^/]+%.wav)', -- wav 11 | '([^/]+%.ogg)' -- ogg 12 | } 13 | 14 | function SERVICE:Match( url ) 15 | url = string.lower(url or "") 16 | 17 | -- check supported encodings 18 | for _, encoding in pairs(SupportedEncodings) do 19 | if url:find(encoding) then 20 | return true 21 | end 22 | end 23 | 24 | return false 25 | end 26 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/base/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | 3 | function SERVICE:Volume( volume ) 4 | if volume then 5 | self._volume = tonumber(volume) or self._volume 6 | end 7 | return self._volume 8 | end 9 | 10 | function SERVICE:IsPaused() 11 | return self._PauseTime ~= nil 12 | end 13 | 14 | function SERVICE:Stop() 15 | self._playing = false 16 | self:emit('stop') 17 | end 18 | 19 | function SERVICE:PlayPause() 20 | if self:IsPlaying() then 21 | self:Pause() 22 | else 23 | self:Play() 24 | end 25 | end 26 | 27 | function SERVICE:Sync() 28 | -- Implement this in timed services 29 | end 30 | 31 | function SERVICE:NetWriteRequest() 32 | -- Send any additional net data here 33 | end 34 | 35 | function SERVICE:OnMousePressed( x, y ) 36 | end 37 | 38 | function SERVICE:OnMouseWheeled( scrollDelta ) 39 | end 40 | 41 | function SERVICE:IsMouseInputEnabled() 42 | return false 43 | end 44 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/base/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "shared.lua" 2 | include "shared.lua" 3 | 4 | local MaxTitleLength = 128 5 | 6 | function SERVICE:SetOwner( ply ) 7 | self._Owner = ply 8 | self._OwnerName = ply:Nick() 9 | self._OwnerSteamID = ply:SteamID() 10 | end 11 | 12 | function SERVICE:GetMetadata( callback ) 13 | 14 | if not self._metadata then 15 | self._metadata = { 16 | title = "Base service", 17 | duration = -1, 18 | url = "", 19 | thumbnail = "" 20 | } 21 | end 22 | 23 | callback(self._metadata) 24 | 25 | end 26 | 27 | local HttpHeaders = { 28 | ["Cache-Control"] = "no-cache", 29 | 30 | -- Keep Alive causes problems on dedicated servers apparently. 31 | -- ["Connection"] = "keep-alive", 32 | 33 | -- Required for Google API requests; uses browser API key. 34 | ["Referer"] = MediaPlayer.GetConfigValue('google.referrer'), 35 | 36 | -- Don't use improperly formatted GMod user agent in case anything actually 37 | -- checks the user agent. 38 | ["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36" 39 | } 40 | 41 | function SERVICE:Fetch( url, onReceive, onFailure, headers ) 42 | 43 | if MediaPlayer.DEBUG then 44 | print( "SERVICE.Fetch", url ) 45 | end 46 | 47 | local request = { 48 | url = url, 49 | method = "GET", 50 | 51 | success = function( code, body, headers ) 52 | if MediaPlayer.DEBUG then 53 | print("HTTP Results["..code.."]:", url) 54 | PrintTable(headers) 55 | print(body) 56 | end 57 | 58 | if isfunction(onReceive) then 59 | onReceive( body, body:len(), headers, code ) 60 | end 61 | end, 62 | 63 | failed = function( err ) 64 | if isfunction(onFailure) then 65 | onFailure( err ) 66 | end 67 | end 68 | } 69 | 70 | -- Pass in extra headers 71 | if headers then 72 | local tbl = table.Copy( HttpHeaders ) 73 | table.Merge( tbl, headers ) 74 | request.headers = tbl 75 | else 76 | request.headers = HttpHeaders 77 | end 78 | 79 | if MediaPlayer.DEBUG then 80 | print "MediaPlayer.Service.Fetch REQUESTING" 81 | PrintTable(request) 82 | end 83 | 84 | HTTP(request) 85 | 86 | end 87 | 88 | function SERVICE:NetReadRequest() 89 | -- Read any additional net data here 90 | end 91 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/base/shared.lua: -------------------------------------------------------------------------------- 1 | local string = string 2 | local urllib = url 3 | local os = os 4 | 5 | local FormatSeconds = MediaPlayerUtils.FormatSeconds 6 | 7 | SERVICE.Name = "Base Service" 8 | SERVICE.Id = "base" 9 | SERVICE.Abstract = true 10 | 11 | -- Inherit EventEmitter for all service instances 12 | EventEmitter:new(SERVICE) 13 | 14 | local OwnerInfoPattern = "%s [%s]" 15 | 16 | function SERVICE:New( url ) 17 | local obj = setmetatable( {}, { 18 | __index = self, 19 | __tostring = self.__tostring 20 | } ) 21 | 22 | obj.url = url 23 | 24 | local success, urlinfo = pcall(urllib.parse2, url) 25 | obj.urlinfo = success and urlinfo or {} 26 | 27 | if CLIENT then 28 | obj._playing = false 29 | obj._volume = 0.33 30 | end 31 | 32 | return obj 33 | end 34 | 35 | function SERVICE:__tostring() 36 | return string.format( '%s, %s, %s', 37 | self:Title(), 38 | FormatSeconds(self:Duration()), 39 | self:OwnerName() ) 40 | end 41 | 42 | -- 43 | -- Determines if the media is valid. 44 | -- 45 | -- @return boolean 46 | -- 47 | function SERVICE:IsValid() 48 | return true 49 | end 50 | 51 | -- 52 | -- Determines if the media supports the given URL. 53 | -- 54 | -- @param url URL. 55 | -- @return boolean 56 | -- 57 | function SERVICE:Match( url ) 58 | return false 59 | end 60 | 61 | -- 62 | -- Gives the unique data used as part of the primary key in the metadata 63 | -- database. 64 | -- 65 | -- @return String 66 | -- 67 | function SERVICE:Data() 68 | return self._data 69 | end 70 | 71 | function SERVICE:Owner() 72 | return self._Owner 73 | end 74 | 75 | SERVICE.GetOwner = SERVICE.Owner 76 | 77 | function SERVICE:OwnerName() 78 | return self._OwnerName or "" 79 | end 80 | 81 | function SERVICE:OwnerSteamID() 82 | return self._OwnerSteamID or "" 83 | end 84 | 85 | function SERVICE:OwnerInfo() 86 | return OwnerInfoPattern:format( self._OwnerName, self._OwnerSteamID ) 87 | end 88 | 89 | function SERVICE:IsOwner( ply ) 90 | return ply == self:GetOwner() or 91 | ply:SteamID() == self:OwnerSteamID() 92 | end 93 | 94 | function SERVICE:Title() 95 | return self._metadata and self._metadata.title or "Unknown" 96 | end 97 | 98 | function SERVICE:Duration( duration ) 99 | if duration then 100 | self._metadata = self._metadata or {} 101 | self._metadata.duration = duration 102 | end 103 | 104 | return self._metadata and self._metadata.duration or -1 105 | end 106 | 107 | -- 108 | -- Determines whether the media is timed. 109 | -- 110 | -- @return boolean 111 | -- 112 | function SERVICE:IsTimed() 113 | return true 114 | end 115 | 116 | function SERVICE:Thumbnail() 117 | return self._metadata and self._metadata.thumbnail 118 | end 119 | 120 | function SERVICE:Url() 121 | return self.url 122 | end 123 | 124 | SERVICE.URL = SERVICE.Url 125 | 126 | function SERVICE:SetMetadata( metadata, new ) 127 | self._metadata = metadata 128 | 129 | if new then 130 | local title = self._metadata.title or "Unknown" 131 | title = title:sub(1, MaxTitleLength) 132 | 133 | -- Escape any '%' char with a letter following it 134 | title = title:gsub('%%%a', '%%%%') 135 | 136 | self._metadata.title = title 137 | end 138 | end 139 | 140 | function SERVICE:SetMetadataValue( key, value ) 141 | if not self._metadata then 142 | self._metadata = {} 143 | end 144 | 145 | self._metadata[key] = value 146 | end 147 | 148 | function SERVICE:GetMetadataValue( key ) 149 | return self._metadata and self._metadata[key] 150 | end 151 | 152 | function SERVICE:UniqueID() 153 | if not self._id then 154 | local data = self:Data() 155 | if not data then 156 | data = util.CRC(self.url) 157 | end 158 | 159 | -- e.g. yt-G2MORmw703o 160 | self._id = string.format( "%s-%s", self.Id, data ) 161 | end 162 | 163 | return self._id 164 | end 165 | 166 | --[[---------------------------------------------------------------------------- 167 | Playback 168 | ------------------------------------------------------------------------------]] 169 | 170 | function SERVICE:StartTime( seconds ) 171 | if type(seconds) == 'number' then 172 | if self._PauseTime then 173 | self._PauseTime = RealTime() 174 | end 175 | 176 | self._StartTime = seconds 177 | end 178 | 179 | if self._PauseTime then 180 | local diff = self._PauseTime - self._StartTime 181 | return RealTime() - diff 182 | else 183 | return self._StartTime 184 | end 185 | end 186 | 187 | function SERVICE:CurrentTime() 188 | if self._StartTime then 189 | if self._PauseTime then 190 | return self._PauseTime - self._StartTime 191 | else 192 | return RealTime() - self._StartTime 193 | end 194 | else 195 | return -1 196 | end 197 | end 198 | 199 | function SERVICE:ResetTime() 200 | self._StartTime = nil 201 | self._PauseTime = nil 202 | end 203 | 204 | function SERVICE:IsPlaying() 205 | return self._playing 206 | end 207 | 208 | function SERVICE:Play() 209 | if self._PauseTime then 210 | -- Update start time to match the time when paused 211 | self._StartTime = RealTime() - (self._PauseTime - self._StartTime) 212 | self._PauseTime = nil 213 | end 214 | 215 | self._playing = true 216 | 217 | if CLIENT then 218 | self:emit('play') 219 | end 220 | end 221 | 222 | function SERVICE:Pause() 223 | self._PauseTime = RealTime() 224 | self._playing = false 225 | 226 | if CLIENT then 227 | self:emit('pause') 228 | end 229 | end 230 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/browser.lua: -------------------------------------------------------------------------------- 1 | DEFINE_BASECLASS( "mp_service_base" ) 2 | 3 | SERVICE.Name = "Browser Base" 4 | SERVICE.Id = "browser" 5 | SERVICE.Abstract = true 6 | 7 | if CLIENT then 8 | 9 | function SERVICE:GetBrowser() 10 | return self.Browser 11 | end 12 | 13 | function SERVICE:OnBrowserReady( browser ) 14 | local resolution = MediaPlayer.Resolution() 15 | local w = resolution * 16/9 16 | local h = resolution 17 | 18 | if IsValid(self.Entity) then 19 | -- normalize resolution to the entity screen size 20 | local config = self.Entity:GetMediaPlayerConfig() 21 | local entwidth = config.width or w 22 | local entheight = config.height or resolution 23 | w = resolution * (entwidth / entheight) 24 | end 25 | 26 | MediaPlayer.SetBrowserSize( browser, w, h ) 27 | 28 | -- Implement this in a child service 29 | end 30 | 31 | function SERVICE:SetVolume( volume ) 32 | -- Implement this in a child service 33 | end 34 | 35 | function SERVICE:Volume( volume ) 36 | local origVolume = volume 37 | 38 | volume = BaseClass.Volume( self, volume ) 39 | 40 | if origVolume and ValidPanel( self.Browser ) then 41 | self:SetVolume( volume ) 42 | end 43 | 44 | return volume 45 | end 46 | 47 | function SERVICE:Play() 48 | 49 | BaseClass.Play( self ) 50 | 51 | if self.Browser and ValidPanel(self.Browser) then 52 | self:OnBrowserReady( self.Browser ) 53 | else 54 | 55 | self._promise = browserpool.get(function( panel ) 56 | 57 | if not panel then 58 | return 59 | end 60 | 61 | if self._promise then 62 | self._promise = nil 63 | end 64 | 65 | self.Browser = panel 66 | self:OnBrowserReady( panel ) 67 | 68 | end) 69 | end 70 | 71 | end 72 | 73 | function SERVICE:Stop() 74 | BaseClass.Stop( self ) 75 | 76 | if self._promise then 77 | self._promise:Cancel('Service has been stopped') 78 | self._promise = nil 79 | end 80 | 81 | if self.Browser then 82 | browserpool.release( self.Browser ) 83 | self.Browser = nil 84 | end 85 | end 86 | 87 | local StartHtml = [[ 88 | 89 | 90 | 91 | 92 | Media Player 93 | 109 | 110 | 111 | ]] 112 | 113 | local EndHtml = [[ 114 | 115 | 116 | ]] 117 | 118 | function SERVICE.WrapHTML( html ) 119 | return table.concat({ StartHtml, html, EndHtml }) 120 | end 121 | 122 | local JS_InjectScript = [[ 123 | (function () { 124 | var script = document.createElement('script'); 125 | script.type = 'text/javascript'; 126 | script.src = '%s'; 127 | document.getElementsByTagName('head')[0].appendChild(script); 128 | }());]] 129 | 130 | function SERVICE:InjectScript( uri ) 131 | self.Browser:QueueJavascript( JS_InjectScript:format( uri ) ) 132 | end 133 | 134 | function SERVICE:OnMousePressed( x, y ) 135 | self.Browser:InjectMouseClick( x, y ) 136 | end 137 | 138 | local SCROLL_MULTIPLIER = -80 139 | function SERVICE:OnMouseWheeled( scrollDelta ) 140 | self.Browser:Scroll( scrollDelta * SCROLL_MULTIPLIER ) 141 | end 142 | 143 | --[[--------------------------------------------------------- 144 | Draw 3D2D 145 | -----------------------------------------------------------]] 146 | 147 | local ValidPanel = ValidPanel 148 | local SetDrawColor = surface.SetDrawColor 149 | local DrawRect = surface.DrawRect 150 | local DrawHTMLPanel = MediaPlayerUtils.DrawHTMLPanel 151 | 152 | function SERVICE:Draw( w, h ) 153 | 154 | if ValidPanel(self.Browser) then 155 | SetDrawColor( 0, 0, 0, 255 ) 156 | DrawRect( 0, 0, w, h ) 157 | DrawHTMLPanel( self.Browser, w, h ) 158 | end 159 | 160 | end 161 | 162 | end 163 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/googledrive/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | 3 | DEFINE_BASECLASS( "mp_service_browser" ) 4 | 5 | -- data:text/html, 6 | 7 | -- https://docs.google.com/file/d/0B1K_ByAqaFKGamdrajd6WXFUSEs0VHI4eTJHNHpPdw/preview 8 | 9 | local EmbedHtml = [[ 10 | ]] 11 | 12 | SERVICE.VideoUrlFormat = "https://video.google.com/get_player?docid=%s&enablejsapi=1&autoplay=1&controls=0&modestbranding=1&rel=0&showinfo=0&wmode=opaque&ps=docs&partnerid=30" 13 | 14 | function SERVICE:OnBrowserReady( browser ) 15 | 16 | BaseClass.OnBrowserReady( self, browser ) 17 | 18 | local fileId = self:GetGoogleDriveFileId() 19 | 20 | local url = self.VideoUrlFormat:format(fileId) 21 | local curTime = self:CurrentTime() 22 | 23 | -- Add start time to URL if the video didn't just begin 24 | if self:IsTimed() and curTime > 3 then 25 | url = url .. "&start=" .. math.Round(curTime) 26 | end 27 | 28 | local html = self.WrapHTML( EmbedHtml:format(url) ) 29 | browser:SetHTML( html ) 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/googledrive/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "shared.lua" 2 | include "shared.lua" 3 | 4 | local TableLookup = MediaPlayerUtils.TableLookup 5 | 6 | -- TODO: 7 | -- https://video.google.com/get_player?wmode=opaque&ps=docs&partnerid=30&docid=0B9Kudw3An4Hnci1VZ0pwcHhJc00&enablejsapi=1 8 | -- http://stackoverflow.com/questions/17779197/google-drive-embed-no-iframe 9 | -- https://developers.google.com/drive/v2/reference/files/get 10 | 11 | local APIKey = MediaPlayer.GetConfigValue('google.api_key') 12 | local MetadataUrl = "https://www.googleapis.com/drive/v2/files/%s?key=%s" 13 | 14 | local SupportedExtensions = { 'mp4' } 15 | 16 | local function OnReceiveMetadata( self, callback, body ) 17 | 18 | local metadata = {} 19 | 20 | local resp = util.JSONToTable( body ) 21 | if not resp then 22 | return callback(false) 23 | end 24 | 25 | if resp.error then 26 | return callback(false, TableLookup(resp, 'error.message')) 27 | end 28 | 29 | local ext = resp.fileExtension or '' 30 | 31 | if not table.HasValue(SupportedExtensions, ext) then 32 | return callback(false, 'MediaPlayer currently only supports .mp4 Google Drive videos') 33 | end 34 | 35 | metadata.title = resp.title 36 | metadata.thumbnail = resp.thumbnailLink 37 | 38 | -- TODO: duration? etc. 39 | -- no duration metadata returned :( 40 | metadata.duration = 3600 * 4 -- default to 4 hours 41 | 42 | self:SetMetadata(metadata, true) 43 | MediaPlayer.Metadata:Save(self) 44 | 45 | callback(self._metadata) 46 | 47 | end 48 | 49 | function SERVICE:GetMetadata( callback ) 50 | if self._metadata then 51 | callback( self._metadata ) 52 | return 53 | end 54 | 55 | local cache = MediaPlayer.Metadata:Query(self) 56 | 57 | if MediaPlayer.DEBUG then 58 | print("MediaPlayer.GetMetadata Cache results:") 59 | PrintTable(cache or {}) 60 | end 61 | 62 | if cache then 63 | 64 | local metadata = {} 65 | metadata.title = cache.title 66 | metadata.duration = tonumber(cache.duration) 67 | metadata.thumbnail = cache.thumbnail 68 | 69 | self:SetMetadata(metadata) 70 | MediaPlayer.Metadata:Save(self) 71 | 72 | callback(self._metadata) 73 | 74 | else 75 | 76 | local fileId = self:GetGoogleDriveFileId() 77 | local apiurl = MetadataUrl:format( fileId, APIKey ) 78 | 79 | self:Fetch( apiurl, 80 | function( body, length, headers, code ) 81 | OnReceiveMetadata( self, callback, body ) 82 | end, 83 | function( code ) 84 | callback(false, "Failed to load YouTube ["..tostring(code).."]") 85 | end 86 | ) 87 | 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/googledrive/shared.lua: -------------------------------------------------------------------------------- 1 | DEFINE_BASECLASS( "mp_service_base" ) 2 | 3 | SERVICE.Name = "Google Drive" 4 | SERVICE.Id = "gd" 5 | SERVICE.Base = "yt" 6 | 7 | local GdFileIdPattern = "[%a%d-_]+" 8 | local UrlSchemes = { 9 | "docs%.google%.com/file/d/" .. GdFileIdPattern .. "/", 10 | "drive%.google%.com/file/d/" .. GdFileIdPattern .. "/" 11 | } 12 | 13 | function SERVICE:New( url ) 14 | local obj = BaseClass.New(self, url) 15 | obj._data = obj:GetGoogleDriveFileId() 16 | return obj 17 | end 18 | 19 | function SERVICE:Match( url ) 20 | for _, pattern in pairs(UrlSchemes) do 21 | if string.find( url, pattern ) then 22 | return true 23 | end 24 | end 25 | return false 26 | end 27 | 28 | function SERVICE:IsTimed() 29 | return true 30 | end 31 | 32 | function SERVICE:GetGoogleDriveFileId() 33 | 34 | local videoId 35 | 36 | if self.videoId then 37 | 38 | videoId = self.videoId 39 | 40 | elseif self.urlinfo then 41 | 42 | local url = self.urlinfo 43 | 44 | -- https://docs.google.com/file/d/(videoId) 45 | if url.path and string.match(url.path, "^/file/d/([%a%d-_]+)") then 46 | videoId = string.match(url.path, "^/file/d/([%a%d-_]+)") 47 | end 48 | 49 | self.videoId = videoId 50 | 51 | end 52 | 53 | return videoId 54 | 55 | end 56 | 57 | -- Used for clientside inheritence of the YouTube service 58 | SERVICE.GetYouTubeVideoId = GetGoogleDriveFileId 59 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/html5_video.lua: -------------------------------------------------------------------------------- 1 | SERVICE.Name = "HTML5 Video" 2 | SERVICE.Id = "h5v" 3 | SERVICE.Base = "res" 4 | 5 | SERVICE.FileExtensions = { 6 | 'webm', 7 | -- 'mp4', -- not yet supported by Awesomium 8 | -- 'ogg' -- already registered as audio, need a work-around :( 9 | } 10 | 11 | DEFINE_BASECLASS( "mp_service_base" ) 12 | 13 | if CLIENT then 14 | 15 | local MimeTypes = { 16 | webm = "video/webm", 17 | mp4 = "video/mp4", 18 | ogg = "video/ogg" 19 | } 20 | 21 | local EmbedHTML = [[ 22 | 27 | ]] 28 | 29 | local JS_Volume = [[(function () { 30 | var elem = document.getElementById('player'); 31 | if (elem) { 32 | elem.volume = %s; 33 | } 34 | }());]] 35 | 36 | function SERVICE:GetHTML() 37 | local url = self.url 38 | 39 | local path = self.urlinfo.path 40 | local ext = path:match("[^/]+%.(%S+)$") 41 | 42 | local mime = MimeTypes[ext] 43 | 44 | return EmbedHTML:format(url, mime) 45 | end 46 | 47 | function SERVICE:Volume( volume ) 48 | local origVolume = volume 49 | 50 | volume = BaseClass.Volume( self, volume ) 51 | 52 | if origVolume and ValidPanel( self.Browser ) then 53 | self.Browser:RunJavascript(JS_Volume:format(volume)) 54 | end 55 | end 56 | 57 | end -------------------------------------------------------------------------------- /lua/mediaplayer/services/image.lua: -------------------------------------------------------------------------------- 1 | SERVICE.Name = "Image" 2 | SERVICE.Id = "img" 3 | SERVICE.Base = "res" 4 | 5 | SERVICE.FileExtensions = { 'png', 'jpg', 'jpeg', 'gif' } 6 | 7 | if CLIENT then 8 | 9 | local EmbedHTML = [[ 10 |
16 |
17 | ]] 18 | 19 | function SERVICE:GetHTML() 20 | return EmbedHTML:format( self.url ) 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /lua/mediaplayer/services/resource/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | 3 | DEFINE_BASECLASS( "mp_service_browser" ) 4 | 5 | function SERVICE:OnBrowserReady( browser ) 6 | BaseClass.OnBrowserReady( self, browser ) 7 | 8 | local html = self:GetHTML() 9 | html = self.WrapHTML( html ) 10 | 11 | self.Browser:SetHTML( html ) 12 | end 13 | 14 | function SERVICE:GetHTML() 15 | return "

SERVICE.GetHTML not yet implemented

" 16 | end 17 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/resource/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "shared.lua" 2 | include "shared.lua" 3 | 4 | local urllib = url 5 | local FilenamePattern = "([^/]+)%.%S+$" 6 | local FilenameExtPattern = "([^/]+%.%S+)$" 7 | 8 | SERVICE.TitleIncludeExtension = true -- include extension in title 9 | 10 | function SERVICE:GetMetadata( callback ) 11 | 12 | if not self._metadata then 13 | 14 | local title 15 | 16 | local pattern = self.TitleIncludeExtension and 17 | FilenameExtPattern or FilenamePattern 18 | 19 | if self.urlinfo.path then 20 | local path = self.urlinfo.path 21 | path = string.match( path, pattern ) -- get filename 22 | 23 | if path then 24 | title = urllib.unescape( path ) 25 | else 26 | title = self.url 27 | end 28 | else 29 | title = self.url 30 | end 31 | 32 | self._metadata = { 33 | title = title or self.Name, 34 | url = self.url 35 | } 36 | 37 | end 38 | 39 | if callback then 40 | callback(self._metadata) 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/resource/shared.lua: -------------------------------------------------------------------------------- 1 | SERVICE.Name = "Resource" 2 | SERVICE.Id = "res" 3 | SERVICE.Base = "browser" 4 | SERVICE.Abstract = true 5 | 6 | SERVICE.FileExtensions = {} 7 | 8 | function SERVICE:Match( url ) 9 | -- check supported file extensions 10 | for _, ext in pairs(self.FileExtensions) do 11 | if url:find("([^/]+%." .. ext .. ")$") then 12 | return true 13 | end 14 | end 15 | 16 | return false 17 | end 18 | 19 | function SERVICE:IsTimed() 20 | return false 21 | end 22 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/shoutcast.lua: -------------------------------------------------------------------------------- 1 | SERVICE.Name = "SHOUTcast" 2 | SERVICE.Id = "shc" 3 | SERVICE.Base = "af" 4 | 5 | -- DEFINE_BASECLASS( "mp_service_af" ) 6 | 7 | local StationUrlPattern = "yp.shoutcast.com/sbin/tunein%-station%.pls%?id=%d+" 8 | 9 | function SERVICE:Match( url ) 10 | return url:match( StationUrlPattern ) 11 | end 12 | 13 | function SERVICE:IsTimed() 14 | return false 15 | end 16 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/soundcloud/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/soundcloud/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "shared.lua" 2 | include "shared.lua" 3 | 4 | local urllib = url 5 | 6 | local ClientId = MediaPlayer.GetConfigValue('soundcloud.client_id') 7 | 8 | -- http://developers.soundcloud.com/docs/api/reference 9 | local MetadataUrl = { 10 | resolve = "http://api.soundcloud.com/resolve.json?url=%s&client_id=" .. ClientId, 11 | tracks = "" 12 | } 13 | 14 | local function OnReceiveMetadata( self, callback, body ) 15 | local resp = util.JSONToTable(body) 16 | if not resp then 17 | callback(false) 18 | return 19 | end 20 | 21 | if resp.errors then 22 | callback(false, "The requested SoundCloud song wasn't found") 23 | return 24 | end 25 | 26 | local artist = resp.user and resp.user.username or "[Unknown artist]" 27 | local stream = resp.stream_url 28 | 29 | if not stream then 30 | callback(false, "The requested SoundCloud song doesn't allow streaming") 31 | return 32 | end 33 | 34 | local thumbnail = resp.artwork_url 35 | if thumbnail then 36 | thumbnail = string.Replace( thumbnail, 'large', 't500x500' ) 37 | end 38 | 39 | -- http://developers.soundcloud.com/docs/api/reference#tracks 40 | local metadata = {} 41 | metadata.title = (resp.title or "[Unknown title]") .. " - " .. artist 42 | metadata.duration = math.ceil(tonumber(resp.duration) / 1000) -- responds in ms 43 | metadata.thumbnail = thumbnail 44 | 45 | metadata.extra = { 46 | stream = stream 47 | } 48 | 49 | self:SetMetadata(metadata, true) 50 | MediaPlayer.Metadata:Save(self) 51 | 52 | self.url = stream .. "?client_id=" .. ClientId 53 | 54 | callback(self._metadata) 55 | end 56 | 57 | function SERVICE:GetMetadata( callback ) 58 | if self._metadata then 59 | callback( self._metadata ) 60 | return 61 | end 62 | 63 | local cache = MediaPlayer.Metadata:Query(self) 64 | 65 | if MediaPlayer.DEBUG then 66 | print("MediaPlayer.GetMetadata Cache results:") 67 | PrintTable(cache or {}) 68 | end 69 | 70 | if cache then 71 | 72 | local metadata = {} 73 | metadata.title = cache.title 74 | metadata.duration = tonumber(cache.duration) 75 | metadata.thumbnail = cache.thumbnail 76 | 77 | metadata.extra = cache.extra 78 | 79 | self:SetMetadata(metadata) 80 | MediaPlayer.Metadata:Save(self) 81 | 82 | if metadata.extra then 83 | local extra = util.JSONToTable(metadata.extra) 84 | 85 | if extra.stream then 86 | self.url = tostring(extra.stream) .. "?client_id=" .. ClientId 87 | end 88 | end 89 | 90 | callback(self._metadata) 91 | 92 | else 93 | 94 | -- TODO: predetermine if we can skip the call to /resolve; check for 95 | -- /track or /playlist in the url path. 96 | 97 | local apiurl = MetadataUrl.resolve:format( self.url ) 98 | 99 | self:Fetch( apiurl, 100 | function( body, length, headers, code ) 101 | OnReceiveMetadata( self, callback, body ) 102 | end, 103 | function( code ) 104 | callback(false, "Failed to load YouTube ["..tostring(code).."]") 105 | end 106 | ) 107 | 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/soundcloud/shared.lua: -------------------------------------------------------------------------------- 1 | DEFINE_BASECLASS( "mp_service_base" ) 2 | 3 | SERVICE.Name = "SoundCloud" 4 | SERVICE.Id = "sc" 5 | SERVICE.Base = "af" 6 | 7 | SERVICE.PrefetchMetadata = false 8 | 9 | function SERVICE:New( url ) 10 | local obj = BaseClass.New(self, url) 11 | 12 | -- TODO: grab id from /tracks/:id, etc. 13 | obj._data = obj.urlinfo.path or '0' 14 | 15 | return obj 16 | end 17 | 18 | function SERVICE:Match( url ) 19 | return string.match( url, "soundcloud.com" ) 20 | end 21 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/twitch/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | 3 | DEFINE_BASECLASS( "mp_service_browser" ) 4 | 5 | local TwitchUrl = "http://www.twitch.tv/%s/%s/%s/popout" 6 | 7 | --- 8 | -- Approximate amount of time it takes for the Twitch video player to load upon 9 | -- loading the webpage. 10 | -- 11 | local playerLoadDelay = 5 12 | 13 | local secMinute = 60 14 | local secHour = secMinute * 60 15 | 16 | local function formatTwitchTime( seconds ) 17 | local hours = math.floor((seconds / secHour) % 24) 18 | local minutes = math.floor((seconds / secMinute) % 60) 19 | seconds = math.floor(seconds % 60) 20 | 21 | local tbl = {} 22 | 23 | if hours > 0 then 24 | table.insert(tbl, hours) 25 | table.insert(tbl, 'h') 26 | end 27 | 28 | if hours > 0 or minutes > 0 then 29 | table.insert(tbl, minutes) 30 | table.insert(tbl, 'm') 31 | end 32 | 33 | table.insert(tbl, seconds) 34 | table.insert(tbl, 's') 35 | 36 | return table.concat(tbl, '') 37 | end 38 | 39 | function SERVICE:OnBrowserReady( browser ) 40 | 41 | BaseClass.OnBrowserReady( self, browser ) 42 | 43 | local info = self:GetTwitchVideoInfo() 44 | local url = TwitchUrl:format(info.channel, info.type, info.chapterId) 45 | 46 | -- Move current time forward due to twitch player load time 47 | local curTime = math.min( self:CurrentTime() + playerLoadDelay, self:Duration() ) 48 | 49 | local time = math.ceil( curTime ) 50 | if time > 5 then 51 | url = url .. '?t=' .. formatTwitchTime(time) 52 | end 53 | 54 | browser:OpenURL( url ) 55 | 56 | end 57 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/twitch/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "shared.lua" 2 | include "shared.lua" 3 | 4 | local urllib = url 5 | 6 | local APIKey = MediaPlayer.GetConfigValue('twitch.client_id') 7 | local MetadataUrl = "https://api.twitch.tv/kraken/videos/%s?client_id=%s" 8 | 9 | local function OnReceiveMetadata( self, callback, body ) 10 | 11 | local metadata = {} 12 | 13 | local response = util.JSONToTable( body ) 14 | if not response then 15 | callback(false) 16 | return 17 | end 18 | 19 | -- Stream invalid 20 | if response.status and response.status == 404 then 21 | return callback( false, "Twitch.TV: " .. tostring(response.message) ) 22 | end 23 | 24 | metadata.title = response.title 25 | metadata.duration = response.length 26 | 27 | -- Add 30 seconds to accomodate for ads in video over 5 minutes 28 | local duration = tonumber(metadata.duration) 29 | if duration and duration > ( 60 * 5 ) then 30 | metadata.duration = duration + 30 31 | end 32 | 33 | metadata.thumbnail = response.preview 34 | 35 | self:SetMetadata(metadata, true) 36 | MediaPlayer.Metadata:Save(self) 37 | 38 | callback(self._metadata) 39 | 40 | end 41 | 42 | function SERVICE:GetMetadata( callback ) 43 | if self._metadata then 44 | callback( self._metadata ) 45 | return 46 | end 47 | 48 | local cache = MediaPlayer.Metadata:Query(self) 49 | 50 | if MediaPlayer.DEBUG then 51 | print("MediaPlayer.GetMetadata Cache results:") 52 | PrintTable(cache or {}) 53 | end 54 | 55 | if cache then 56 | 57 | local metadata = {} 58 | metadata.title = cache.title 59 | metadata.duration = cache.duration 60 | metadata.thumbnail = cache.thumbnail 61 | 62 | self:SetMetadata(metadata) 63 | MediaPlayer.Metadata:Save(self) 64 | 65 | callback(self._metadata) 66 | 67 | else 68 | 69 | local info = self:GetTwitchVideoInfo() 70 | 71 | -- API call fix 72 | if info.type == 'b' then 73 | info.type = 'a' 74 | end 75 | 76 | local apiurl = MetadataUrl:format( info.type .. info.chapterId, APIKey ) 77 | 78 | self:Fetch( apiurl, 79 | function( body, length, headers, code ) 80 | OnReceiveMetadata( self, callback, body ) 81 | end, 82 | function( code ) 83 | callback(false, "Failed to load Twitch.TV ["..tostring(code).."]") 84 | end, 85 | 86 | -- Twitch.TV API v3 headers 87 | { 88 | ["Accept"] = "application/vnd.twitchtv.v3+json" 89 | } 90 | ) 91 | 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/twitch/shared.lua: -------------------------------------------------------------------------------- 1 | DEFINE_BASECLASS( "mp_service_base" ) 2 | 3 | SERVICE.Name = "Twitch.TV - Video" 4 | SERVICE.Id = "twv" 5 | SERVICE.Base = "browser" 6 | 7 | function SERVICE:New( url ) 8 | local obj = BaseClass.New(self, url) 9 | 10 | local info = obj:GetTwitchVideoInfo() 11 | obj._data = info.channel .. "_" .. info.chapterId 12 | 13 | return obj 14 | end 15 | 16 | function SERVICE:Match( url ) 17 | -- TODO: should the parsed url be passed instead? 18 | return (string.match(url, "justin.tv") or 19 | string.match(url, "twitch.tv")) and 20 | string.match(url, ".tv/[%w_]+/%a/%d+") 21 | end 22 | 23 | function SERVICE:GetTwitchVideoInfo() 24 | 25 | local info 26 | 27 | if self._twitchInfo then 28 | 29 | info = self._twitchInfo 30 | 31 | elseif self.urlinfo then 32 | 33 | local url = self.urlinfo 34 | 35 | local channel, type, chapterId = string.match(url.path, "^/([%w_]+)/(%a)/(%d+)") 36 | 37 | -- Chapter videos use /c/ while archived videos use /b/ 38 | if type ~= "c" then 39 | type = "b" 40 | end 41 | 42 | info = { 43 | channel = channel, 44 | type = type, 45 | chapterId = chapterId 46 | } 47 | 48 | self._twitchInfo = info 49 | 50 | end 51 | 52 | return info 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/twitchstream/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | 3 | local htmlBaseUrl = MediaPlayer.GetConfigValue('html.base_url') 4 | 5 | DEFINE_BASECLASS( "mp_service_browser" ) 6 | 7 | local TwitchUrl = "http://www.twitch.tv/%s/popout" 8 | 9 | local JS_Play = "if(window.MediaPlayer) MediaPlayer.play();" 10 | local JS_Pause = "if(window.MediaPlayer) MediaPlayer.pause();" 11 | 12 | local JS_HideControls = [[ 13 | document.body.style.cssText = 'overflow:hidden;height:106.8% !important';]] 14 | 15 | function SERVICE:OnBrowserReady( browser ) 16 | 17 | BaseClass.OnBrowserReady( self, browser ) 18 | 19 | local channel = self:GetTwitchChannel() 20 | local url = TwitchUrl:format(channel) 21 | 22 | browser:OpenURL( url ) 23 | 24 | browser:QueueJavascript( JS_HideControls ) 25 | self:InjectScript( htmlBaseUrl .. "scripts/services/twitch.js" ) 26 | 27 | end 28 | 29 | function SERVICE:Pause() 30 | BaseClass.Pause( self ) 31 | 32 | if ValidPanel(self.Browser) then 33 | self.Browser:RunJavascript(JS_Pause) 34 | self._YTPaused = true 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/twitchstream/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "shared.lua" 2 | include "shared.lua" 3 | 4 | local urllib = url 5 | 6 | local APIKey = MediaPlayer.GetConfigValue('twitch.client_id') 7 | local MetadataUrl = "https://api.twitch.tv/kraken/streams/%s?client_id=%s" 8 | 9 | local function OnReceiveMetadata( self, callback, body ) 10 | 11 | local metadata = {} 12 | 13 | local response = util.JSONToTable( body ) 14 | if not response then 15 | callback(false) 16 | return 17 | end 18 | 19 | local stream = response.stream 20 | 21 | -- Stream offline 22 | if not stream then 23 | return callback( false, "Twitch.TV: The requested stream was offline" ) 24 | end 25 | 26 | local channel = stream.channel 27 | local status = channel and channel.status or "Twitch.TV Stream" 28 | 29 | metadata.title = status 30 | metadata.thumbnail = stream.preview.medium 31 | 32 | self:SetMetadata(metadata, true) 33 | 34 | callback(self._metadata) 35 | 36 | end 37 | 38 | function SERVICE:GetMetadata( callback ) 39 | 40 | if self._metadata then 41 | callback( self._metadata ) 42 | return 43 | end 44 | 45 | local channel = self:GetTwitchChannel() 46 | local apiurl = MetadataUrl:format( channel, APIKey ) 47 | 48 | self:Fetch( apiurl, 49 | function( body, length, headers, code ) 50 | OnReceiveMetadata( self, callback, body ) 51 | end, 52 | function( code ) 53 | callback(false, "Failed to load Twitch.TV ["..tostring(code).."]") 54 | end, 55 | 56 | -- Twitch.TV API v3 headers 57 | { 58 | ["Accept"] = "application/vnd.twitchtv.v3+json" 59 | } 60 | ) 61 | 62 | end -------------------------------------------------------------------------------- /lua/mediaplayer/services/twitchstream/shared.lua: -------------------------------------------------------------------------------- 1 | DEFINE_BASECLASS( "mp_service_browser" ) 2 | 3 | SERVICE.Name = "Twitch.TV - Stream" 4 | SERVICE.Id = "twl" 5 | SERVICE.Base = "browser" 6 | 7 | function SERVICE:New( url ) 8 | local obj = BaseClass.New(self, url) 9 | 10 | local channel = obj:GetTwitchChannel() 11 | obj._data = channel 12 | 13 | return obj 14 | end 15 | 16 | function SERVICE:Match( url ) 17 | return string.match(url, "twitch.tv") and 18 | string.match(url, ".tv/[%w_]+$") 19 | end 20 | 21 | function SERVICE:IsTimed() 22 | return false 23 | end 24 | 25 | function SERVICE:GetTwitchChannel() 26 | 27 | local channel 28 | 29 | if self._twitchChannel then 30 | 31 | channel = self._twitchChannel 32 | 33 | elseif self.urlinfo then 34 | 35 | local url = self.urlinfo 36 | 37 | channel = string.match(url.path, "^/([%w_]+)") 38 | self._twitchChannel = channel 39 | 40 | end 41 | 42 | return channel 43 | 44 | end 45 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/vimeo/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | 3 | DEFINE_BASECLASS( "mp_service_browser" ) 4 | 5 | local JS_SetVolume = "if(window.MediaPlayer) MediaPlayer.setVolume(%s);" 6 | local JS_Seek = "if(window.MediaPlayer) MediaPlayer.seek(%s);" 7 | 8 | local function VimeoSetVolume( self ) 9 | if not self.Browser then return end 10 | local js = JS_SetVolume:format( MediaPlayer.Volume() ) 11 | self.Browser:RunJavascript(js) 12 | end 13 | 14 | local function VimeoSeek( self, seekTime ) 15 | if not self.Browser then return end 16 | local js = JS_Seek:format( seekTime ) 17 | self.Browser:RunJavascript(js) 18 | end 19 | 20 | function SERVICE:SetVolume( volume ) 21 | VimeoSetVolume( self ) 22 | end 23 | 24 | function SERVICE:OnBrowserReady( browser ) 25 | 26 | BaseClass.OnBrowserReady( self, browser ) 27 | 28 | local videoId = self:GetVimeoVideoId() 29 | 30 | -- local url = VimeoVideoUrl:format( videoId ) 31 | -- browser:OpenURL( url ) 32 | 33 | -- browser:QueueJavascript( JS_Init ) 34 | 35 | -- local html = EmbedHTML:format( videoId ) 36 | -- html = self.WrapHTML( html ) 37 | -- browser:SetHTML( html ) 38 | 39 | local url = "http://localhost/vimeo.html#" .. videoId 40 | browser:OpenURL( url ) 41 | 42 | end 43 | 44 | function SERVICE:Sync() 45 | local seekTime = self:CurrentTime() 46 | if seekTime > 0 then 47 | VimeoSeek( self, seekTime ) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/vimeo/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "shared.lua" 2 | include "shared.lua" 3 | 4 | local MetadataUrl = "http://vimeo.com/api/v2/video/%s.json" 5 | 6 | local function OnReceiveMetadata( self, callback, body ) 7 | 8 | local metadata = {} 9 | 10 | local data = util.JSONToTable( body ) 11 | if not data then 12 | return callback( false, "Failed to parse video's metadata response." ) 13 | end 14 | 15 | data = data[1] 16 | 17 | metadata.title = data.title 18 | metadata.duration = data.duration 19 | metadata.thumbnail = data.thumbnail_medium 20 | 21 | self:SetMetadata(metadata, true) 22 | MediaPlayer.Metadata:Save(self) 23 | 24 | callback(self._metadata) 25 | 26 | end 27 | 28 | function SERVICE:GetMetadata( callback ) 29 | if self._metadata then 30 | callback( self._metadata ) 31 | return 32 | end 33 | 34 | local cache = MediaPlayer.Metadata:Query(self) 35 | 36 | if MediaPlayer.DEBUG then 37 | print("MediaPlayer.GetMetadata Cache results:") 38 | PrintTable(cache or {}) 39 | end 40 | 41 | if cache then 42 | 43 | local metadata = {} 44 | metadata.title = cache.title 45 | metadata.duration = cache.duration 46 | metadata.thumbnail = cache.thumbnail 47 | 48 | self:SetMetadata(metadata) 49 | MediaPlayer.Metadata:Save(self) 50 | 51 | callback(self._metadata) 52 | 53 | else 54 | 55 | local videoId = self:GetVimeoVideoId() 56 | local apiurl = MetadataUrl:format( videoId ) 57 | 58 | self:Fetch( apiurl, 59 | function( body, length, headers, code ) 60 | OnReceiveMetadata( self, callback, body ) 61 | end, 62 | function( code ) 63 | callback(false, "Failed to load Vimeo ["..tostring(code).."]") 64 | end 65 | ) 66 | 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/vimeo/shared.lua: -------------------------------------------------------------------------------- 1 | DEFINE_BASECLASS( "mp_service_browser" ) 2 | 3 | SERVICE.Name = "Vimeo" 4 | SERVICE.Id = "vm" 5 | SERVICE.Base = "browser" 6 | 7 | function SERVICE:New( url ) 8 | local obj = BaseClass.New(self, url) 9 | obj._data = obj:GetVimeoVideoId() 10 | return obj 11 | end 12 | 13 | function SERVICE:Match( url ) 14 | return string.find( url, "vimeo.com/%d+" ) 15 | end 16 | 17 | function SERVICE:GetVimeoVideoId() 18 | 19 | local videoId 20 | 21 | if self.videoId then 22 | 23 | videoId = self.videoId 24 | 25 | elseif self.urlinfo then 26 | 27 | local url = self.urlinfo 28 | 29 | -- http://www.vimeo.com/(videoId) 30 | videoId = string.match(url.path, "^/(%d+)") 31 | 32 | self.videoId = videoId 33 | 34 | end 35 | 36 | return videoId 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/webpage.lua: -------------------------------------------------------------------------------- 1 | DEFINE_BASECLASS( "mp_service_browser" ) 2 | 3 | SERVICE.Name = "Webpage" 4 | SERVICE.Id = "www" 5 | SERVICE.Base = "res" 6 | SERVICE.Abstract = true -- This service must be handled as a special case. 7 | 8 | if CLIENT then 9 | 10 | function SERVICE:OnBrowserReady( browser ) 11 | BaseClass.OnBrowserReady( self, browser ) 12 | browser:OpenURL( self.url ) 13 | end 14 | 15 | function SERVICE:IsMouseInputEnabled() 16 | return IsValid( self.Browser ) 17 | end 18 | 19 | else 20 | 21 | function SERVICE:Match( url ) 22 | return false 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/youtube/cl_init.lua: -------------------------------------------------------------------------------- 1 | include "shared.lua" 2 | 3 | local urllib = url 4 | 5 | local htmlBaseUrl = MediaPlayer.GetConfigValue('html.base_url') 6 | 7 | DEFINE_BASECLASS( "mp_service_browser" ) 8 | 9 | -- https://developers.google.com/youtube/player_parameters 10 | -- TODO: add closed caption option according to cvar 11 | SERVICE.VideoUrlFormat = htmlBaseUrl .. "youtube.html" 12 | 13 | local JS_SetVolume = "if(window.MediaPlayer) MediaPlayer.setVolume(%s);" 14 | local JS_Seek = "if(window.MediaPlayer) MediaPlayer.seek(%s);" 15 | local JS_Play = "if(window.MediaPlayer) MediaPlayer.play();" 16 | local JS_Pause = "if(window.MediaPlayer) MediaPlayer.pause();" 17 | 18 | local function YTSetVolume( self ) 19 | -- if not self.playerId then return end 20 | local js = JS_SetVolume:format( MediaPlayer.Volume() * 100 ) 21 | if self.Browser then 22 | self.Browser:RunJavascript(js) 23 | end 24 | end 25 | 26 | local function YTSeek( self, seekTime ) 27 | -- if not self.playerId then return end 28 | local js = JS_Seek:format( seekTime ) 29 | if self.Browser then 30 | self.Browser:RunJavascript(js) 31 | end 32 | end 33 | 34 | function SERVICE:SetVolume( volume ) 35 | local js = JS_SetVolume:format( MediaPlayer.Volume() * 100 ) 36 | self.Browser:RunJavascript(js) 37 | end 38 | 39 | function SERVICE:OnBrowserReady( browser ) 40 | 41 | BaseClass.OnBrowserReady( self, browser ) 42 | 43 | -- Resume paused player 44 | if self._YTPaused then 45 | self.Browser:RunJavascript( JS_Play ) 46 | self._YTPaused = nil 47 | return 48 | end 49 | 50 | local videoId = self:GetYouTubeVideoId() 51 | local timedParam = self:IsTimed() and '1' or '0' 52 | local url = self.VideoUrlFormat .. '?v=' .. videoId .. 53 | '&timed=' .. timedParam 54 | 55 | local curTime = self:CurrentTime() 56 | 57 | -- Add start time to URL if the video didn't just begin 58 | if self:IsTimed() and curTime > 3 then 59 | url = url .. "&start=" .. math.Round(curTime) 60 | end 61 | 62 | browser:OpenURL(url) 63 | 64 | end 65 | 66 | function SERVICE:Pause() 67 | BaseClass.Pause( self ) 68 | 69 | if ValidPanel(self.Browser) then 70 | self.Browser:RunJavascript(JS_Pause) 71 | self._YTPaused = true 72 | end 73 | end 74 | 75 | function SERVICE:Sync() 76 | local seekTime = self:CurrentTime() 77 | if self:IsPlaying() and self:IsTimed() and seekTime > 0 then 78 | YTSeek( self, seekTime ) 79 | end 80 | end 81 | 82 | function SERVICE:IsMouseInputEnabled() 83 | return IsValid( self.Browser ) 84 | end 85 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/youtube/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "shared.lua" 2 | include "shared.lua" 3 | 4 | local TableLookup = MediaPlayerUtils.TableLookup 5 | local htmlentities_decode = url.htmlentities_decode 6 | 7 | --- 8 | -- Helper function for converting ISO 8601 time strings; this is the formatting 9 | -- used for duration specified in the YouTube v3 API. 10 | -- 11 | -- http://stackoverflow.com/a/22149575/1490006 12 | -- 13 | local function convertISO8601Time( duration ) 14 | local a = {} 15 | 16 | for part in string.gmatch(duration, "%d+") do 17 | table.insert(a, part) 18 | end 19 | 20 | if duration:find('M') and not (duration:find('H') or duration:find('S')) then 21 | a = {0, a[1], 0} 22 | end 23 | 24 | if duration:find('H') and not duration:find('M') then 25 | a = {a[1], 0, a[2]} 26 | end 27 | 28 | if duration:find('H') and not (duration:find('M') or duration:find('S')) then 29 | a = {a[1], 0, 0} 30 | end 31 | 32 | duration = 0 33 | 34 | if #a == 3 then 35 | duration = duration + tonumber(a[1]) * 3600 36 | duration = duration + tonumber(a[2]) * 60 37 | duration = duration + tonumber(a[3]) 38 | end 39 | 40 | if #a == 2 then 41 | duration = duration + tonumber(a[1]) * 60 42 | duration = duration + tonumber(a[2]) 43 | end 44 | 45 | if #a == 1 then 46 | duration = duration + tonumber(a[1]) 47 | end 48 | 49 | return duration 50 | end 51 | 52 | function SERVICE:GetMetadata( callback ) 53 | if self._metadata then 54 | callback( self._metadata ) 55 | return 56 | end 57 | 58 | local cache = MediaPlayer.Metadata:Query(self) 59 | 60 | if MediaPlayer.DEBUG then 61 | print("MediaPlayer.GetMetadata Cache results:") 62 | PrintTable(cache or {}) 63 | end 64 | 65 | if cache then 66 | 67 | local metadata = {} 68 | 69 | metadata.title = cache.title 70 | metadata.duration = tonumber(cache.duration) 71 | metadata.thumbnail = cache.thumbnail 72 | 73 | self:SetMetadata(metadata) 74 | 75 | if self:IsTimed() then 76 | MediaPlayer.Metadata:Save(self) 77 | end 78 | 79 | callback(self._metadata) 80 | 81 | else 82 | local videoId = self:GetYouTubeVideoId() 83 | local videoUrl = "https://www.youtube.com/watch?v="..videoId 84 | 85 | self:Fetch( videoUrl, 86 | -- On Success 87 | function( body, length, headers, code ) 88 | local status, metadata = pcall(self.ParseYTMetaDataFromHTML, self, body) 89 | 90 | -- html couldn't be parsed 91 | if not status or not metadata.title or not isnumber(metadata.duration) then 92 | -- Title is nil or Duration is nan 93 | if istable(metadata) then 94 | metadata = "title = "..type(metadata.title)..", duration = "..type(metadata.duration) 95 | end 96 | -- Misc error 97 | callback(false, "Failed to parse HTML Page for metadata: "..metadata) 98 | return 99 | end 100 | 101 | self:SetMetadata(metadata, true) 102 | 103 | if self:IsTimed() then 104 | MediaPlayer.Metadata:Save(self) 105 | end 106 | 107 | callback(self._metadata) 108 | end, 109 | -- On failure 110 | function( reason ) 111 | callback(false, "Failed to fetch YouTube HTTP metadata [reason="..tostring(reason).."]") 112 | end, 113 | -- Headers 114 | { 115 | ["User-Agent"] = "Googlebot" 116 | } 117 | ) 118 | end 119 | end 120 | 121 | --- 122 | -- Get the value for an attribute from a html element 123 | -- 124 | local function ParseElementAttribute( element, attribute ) 125 | if not element then return end 126 | -- Find the desired attribute 127 | local output = string.match( element, attribute.."%s-=%s-%b\"\"" ) 128 | if not output then return end 129 | -- Remove the 'attribute=' part 130 | output = string.gsub( output, attribute.."%s-=%s-", "" ) 131 | -- Trim the quotes around the value string 132 | return string.sub( output, 2, -2 ) 133 | end 134 | 135 | --- 136 | -- Get the contents of a html element by removing tags 137 | -- Used as fallback for when title cannot be found 138 | -- 139 | local function ParseElementContent( element ) 140 | if not element then return end 141 | -- Trim start 142 | local output = string.gsub( element, "^%s-<%w->%s-", "" ) 143 | -- Trim end 144 | return string.gsub( output, "%s-%s-$", "" ) 145 | end 146 | 147 | -- Lua search patterns to find metadata from the html 148 | local patterns = { 149 | ["title"] = "", 150 | ["title_fallback"] = ".-", 151 | ["thumb"] = "", 152 | ["thumb_fallback"] = "", 153 | ["duration"] = "", 154 | ["live"] = "", 155 | ["live_enddate"] = "" 156 | } 157 | 158 | --- 159 | -- Function to parse video metadata straight from the html instead of using the API 160 | -- 161 | function SERVICE:ParseYTMetaDataFromHTML( html ) 162 | --MetaData table to return when we're done 163 | local metadata = {} 164 | 165 | -- Fetch title and thumbnail, with fallbacks if needed 166 | metadata.title = ParseElementAttribute(string.match(html, patterns["title"]), "content") 167 | or ParseElementContent(string.match(html, patterns["title_fallback"])) 168 | 169 | -- Parse HTML entities in the title into symbols 170 | metadata.title = htmlentities_decode(metadata.title) 171 | 172 | metadata.thumbnail = ParseElementAttribute(string.match(html, patterns["thumb"]), "content") 173 | or ParseElementAttribute(string.match(html, patterns["thumb_fallback"]), "href") 174 | 175 | -- See if the video is an ongoing live broadcast 176 | -- Set duration to 0 if it is, otherwise use the actual duration 177 | local isLiveBroadcast = tobool(ParseElementAttribute(string.match(html, patterns["live"]), "content")) 178 | local broadcastEndDate = string.match(html, patterns["live_enddate"]) 179 | if isLiveBroadcast and not broadcastEndDate then 180 | -- Mark as live video 181 | metadata.duration = 0 182 | else 183 | local durationISO8601 = ParseElementAttribute(string.match(html, patterns["duration"]), "content") 184 | if isstring(durationISO8601) then 185 | metadata.duration = math.max(1, convertISO8601Time(durationISO8601)) 186 | end 187 | end 188 | 189 | return metadata 190 | end 191 | -------------------------------------------------------------------------------- /lua/mediaplayer/services/youtube/shared.lua: -------------------------------------------------------------------------------- 1 | DEFINE_BASECLASS( "mp_service_base" ) 2 | 3 | SERVICE.Name = "YouTube" 4 | SERVICE.Id = "yt" 5 | SERVICE.Base = "browser" 6 | 7 | local YtVideoIdPattern = "[%a%d-_]+" 8 | local UrlSchemes = { 9 | "youtube%.com/watch%?v=" .. YtVideoIdPattern, 10 | "youtu%.be/watch%?v=" .. YtVideoIdPattern, 11 | "youtube%.com/v/" .. YtVideoIdPattern, 12 | "youtu%.be/v/" .. YtVideoIdPattern, 13 | "youtube%.googleapis%.com/v/" .. YtVideoIdPattern 14 | } 15 | 16 | function SERVICE:New( url ) 17 | local obj = BaseClass.New(self, url) 18 | obj._data = obj:GetYouTubeVideoId() 19 | return obj 20 | end 21 | 22 | function SERVICE:Match( url ) 23 | for _, pattern in pairs(UrlSchemes) do 24 | if string.find( url, pattern ) then 25 | return true 26 | end 27 | end 28 | 29 | return false 30 | end 31 | 32 | function SERVICE:IsTimed() 33 | if self._istimed == nil then 34 | -- YouTube Live resolves to 0 second video duration 35 | self._istimed = self:Duration() > 0 36 | end 37 | 38 | return self._istimed 39 | end 40 | 41 | function SERVICE:GetYouTubeVideoId() 42 | 43 | local videoId 44 | 45 | if self.videoId then 46 | 47 | videoId = self.videoId 48 | 49 | elseif self.urlinfo then 50 | 51 | local url = self.urlinfo 52 | 53 | -- http://www.youtube.com/watch?v=(videoId) 54 | if url.query and url.query.v then 55 | videoId = url.query.v 56 | 57 | -- http://www.youtube.com/v/(videoId) 58 | elseif url.path and string.match(url.path, "^/v/([%a%d-_]+)") then 59 | videoId = string.match(url.path, "^/v/([%a%d-_]+)") 60 | 61 | -- http://youtube.googleapis.com/v/(videoId) 62 | elseif url.path and string.match(url.path, "^/v/([%a%d-_]+)") then 63 | videoId = string.match(url.path, "^/v/([%a%d-_]+)") 64 | 65 | -- http://youtu.be/(videoId) 66 | elseif string.match(url.host, "youtu.be") and 67 | url.path and string.match(url.path, "^/([%a%d-_]+)$") and 68 | ( (not url.query) or #url.query == 0 ) then -- short url 69 | 70 | videoId = string.match(url.path, "^/([%a%d-_]+)$") 71 | end 72 | 73 | self.videoId = videoId 74 | 75 | end 76 | 77 | return videoId 78 | 79 | end 80 | -------------------------------------------------------------------------------- /lua/mediaplayer/sh_cvars.lua: -------------------------------------------------------------------------------- 1 | MediaPlayer.Cvars = {} 2 | 3 | MediaPlayer.Cvars.Debug = CreateConVar( "mediaplayer_debug", 0, FCVAR_DONTRECORD, "Enables media player debug mode; logs a bunch of actions into the console." ) 4 | MediaPlayer.DEBUG = MediaPlayer.Cvars.Debug:GetBool() 5 | cvars.AddChangeCallback( "mediaplayer_debug", function(name, old, new) 6 | MediaPlayer.DEBUG = new == 1 7 | end) 8 | 9 | MediaPlayer.Cvars.AllowWebpages = CreateConVar( "mediaplayer_allow_webpages", 0, { 10 | FCVAR_ARCHIVE, 11 | FCVAR_NOTIFY, 12 | FCVAR_REPLICATED, 13 | FCVAR_SERVER_CAN_EXECUTE 14 | }, "Allows any webpage to be requested." ) 15 | 16 | MediaPlayer.Cvars.QueueLimit = CreateConVar( "mediaplayer_queue_limit", 64, { 17 | FCVAR_REPLICATED, 18 | FCVAR_SERVER_CAN_EXECUTE 19 | }, "Maximum size of a media player queue." ) 20 | 21 | if CLIENT then 22 | 23 | MediaPlayer.Cvars.Resolution = CreateClientConVar( "mediaplayer_resolution", 480, true, false ) 24 | MediaPlayer.Cvars.Audio3D = CreateClientConVar( "mediaplayer_3daudio", 1, true, false ) 25 | MediaPlayer.Cvars.Volume = CreateClientConVar( "mediaplayer_volume", 0.15, true, false ) 26 | MediaPlayer.Cvars.MuteUnfocused = CreateClientConVar( "mediaplayer_mute_unfocused", 1, true, false ) 27 | MediaPlayer.Cvars.Fullscreen = CreateClientConVar( "mediaplayer_fullscreen", 0, false, false ) 28 | MediaPlayer.Cvars.DrawThumbnails = CreateClientConVar( "mediaplayer_draw_thumbnails", 0, true, false ) 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lua/mediaplayer/sh_events.lua: -------------------------------------------------------------------------------- 1 | MP.EVENTS = { 2 | MEDIA_CHANGED = "mediaChanged", 3 | QUEUE_CHANGED = "mp.events.queueChanged", 4 | PLAYER_STATE_CHANGED = "mp.events.playerStateChanged" 5 | } 6 | 7 | if CLIENT then 8 | 9 | table.Merge( MP.EVENTS, { 10 | VOLUME_CHANGED = "mp.events.volumeChanged" 11 | } ) 12 | 13 | end 14 | -------------------------------------------------------------------------------- /lua/mediaplayer/sh_history.lua: -------------------------------------------------------------------------------- 1 | --[[--------------------------------------------------------- 2 | Media Player History 3 | -----------------------------------------------------------]] 4 | 5 | MediaPlayer.History = {} 6 | 7 | --- 8 | -- Default metadata table name 9 | -- @type String 10 | -- 11 | local TableName = "mediaplayer_history" 12 | 13 | --- 14 | -- SQLite table struct 15 | -- @type String 16 | -- 17 | local TableStruct = string.format([[ 18 | CREATE TABLE %s ( 19 | id INTEGER PRIMARY KEY AUTOINCREMENT, 20 | mediaid VARCHAR(48), 21 | url VARCHAR(512), 22 | player_name VARCHAR(32), 23 | steamid VARCHAR(32), 24 | time DATETIME DEFAULT CURRENT_TIMESTAMP 25 | )]], TableName) 26 | 27 | --- 28 | -- Default number of results to return 29 | -- @type Integer 30 | -- 31 | local DefaultResultLimit = 100 32 | 33 | --- 34 | -- Log the given media as a request. 35 | -- 36 | -- @param media Media service object. 37 | -- @return table SQL query results. 38 | -- 39 | function MediaPlayer.History:LogRequest( media ) 40 | local id = media:UniqueID() 41 | if not id then return end 42 | 43 | local ply = media:GetOwner() 44 | if not IsValid(ply) then return end 45 | 46 | local query = string.format( "INSERT INTO `%s` " .. 47 | "(mediaid,url,player_name,steamid) " .. 48 | "VALUES ('%s',%s,%s,'%s')", 49 | TableName, 50 | media:UniqueID(), 51 | sql.SQLStr( media:Url() ), 52 | sql.SQLStr( ply:Nick() ), 53 | ply:SteamID64() or -1 ) 54 | 55 | local result = sql.Query(query) 56 | 57 | if MediaPlayer.DEBUG then 58 | print("MediaPlayer.History.LogRequest") 59 | print(query) 60 | if istable(result) then 61 | PrintTable(result) 62 | else 63 | print(result) 64 | end 65 | end 66 | 67 | return result 68 | end 69 | 70 | function MediaPlayer.History:GetRequestsByPlayer( ply, limit ) 71 | if not isnumber(limit) then 72 | limit = DefaultResultLimit 73 | end 74 | 75 | local query = string.format( [[ 76 | SELECT H.*, M.title, M.thumbnail, M.duration 77 | FROM %s AS H 78 | JOIN mediaplayer_metadata AS M 79 | ON (M.id = H.mediaid) 80 | WHERE steamid='%s' 81 | LIMIT %d]], 82 | TableName, 83 | ply:SteamID64() or -1, 84 | limit ) 85 | 86 | local result = sql.Query(query) 87 | 88 | if MediaPlayer.DEBUG then 89 | print("MediaPlayer.History.GetRequestsByPlayer", ply, limit) 90 | print(query) 91 | if istable(result) then 92 | PrintTable(result) 93 | else 94 | print(result) 95 | end 96 | end 97 | 98 | return result 99 | end 100 | 101 | -- Create the SQLite table if it doesn't exist 102 | if not sql.TableExists(TableName) then 103 | Msg("MediaPlayer.History: Creating `" .. TableName .. "` table...\n") 104 | print(sql.Query(TableStruct)) 105 | end 106 | -------------------------------------------------------------------------------- /lua/mediaplayer/sh_metadata.lua: -------------------------------------------------------------------------------- 1 | --[[--------------------------------------------------------- 2 | Media Player Metadata 3 | 4 | All media metadata is cached in an SQLite table for quick 5 | lookup and to prevent unnecessary network requests. 6 | -----------------------------------------------------------]] 7 | 8 | MediaPlayer.Metadata = {} 9 | 10 | --- 11 | -- Default metadata table name 12 | -- @type String 13 | -- 14 | local TableName = "mediaplayer_metadata" 15 | 16 | --- 17 | -- SQLite table struct 18 | -- @type String 19 | -- 20 | local TableStruct = string.format([[ 21 | CREATE TABLE %s ( 22 | id VARCHAR(48) PRIMARY KEY, 23 | title VARCHAR(128), 24 | duration INTEGER NOT NULL DEFAULT 0, 25 | thumbnail VARCHAR(512), 26 | extra VARCHAR(2048), 27 | request_count INTEGER NOT NULL DEFAULT 1, 28 | last_request INTEGER NOT NULL DEFAULT 0, 29 | last_updated INTEGER NOT NULL DEFAULT 0, 30 | expired BOOLEAN NOT NULL DEFAULT 0 31 | )]], TableName) 32 | 33 | --- 34 | -- Maximum cache age before it expires; currently one week in seconds. 35 | -- @type Number 36 | -- 37 | local MaxCacheAge = 604800 38 | 39 | --- 40 | -- Query the metadata table for the given media object's metadata. 41 | -- If the metadata is older than one week, it is ignored and replaced upon 42 | -- saving. 43 | -- 44 | -- @param media Media service object. 45 | -- @return table Cached metadata results. 46 | -- 47 | function MediaPlayer.Metadata:Query( media ) 48 | local id = media:UniqueID() 49 | if not id then return end 50 | 51 | local query = ("SELECT * FROM `%s` WHERE id='%s'"):format(TableName, id) 52 | 53 | if MediaPlayer.DEBUG then 54 | print("MediaPlayer.Metadata.Query") 55 | print(query) 56 | end 57 | 58 | local results = sql.QueryRow(query) 59 | 60 | if results then 61 | local expired = ( tonumber(results.expired) == 1 ) 62 | 63 | -- Media metadata has been marked as out-of-date 64 | if expired then 65 | return nil 66 | end 67 | 68 | local lastupdated = tonumber( results.last_updated ) 69 | local timediff = os.time() - lastupdated 70 | 71 | if timediff > MaxCacheAge then 72 | 73 | -- Set metadata entry as expired 74 | query = "UPDATE `%s` SET expired=1 WHERE id='%s'" 75 | query = query:format( TableName, id ) 76 | 77 | if MediaPlayer.DEBUG then 78 | print("MediaPlayer.Metadata.Query: Setting entry as expired") 79 | print(query) 80 | end 81 | 82 | sql.Query( query ) 83 | 84 | return nil 85 | 86 | else 87 | return results 88 | end 89 | elseif results == false then 90 | ErrorNoHalt("MediaPlayer.Metadata.Query: There was an error executing the SQL query\n") 91 | print(query) 92 | end 93 | 94 | return nil 95 | end 96 | 97 | --- 98 | -- Save or update the given media object into the metadata table. 99 | -- 100 | -- @param media Media service object. 101 | -- @return table SQL query results. 102 | -- 103 | function MediaPlayer.Metadata:Save( media ) 104 | local id = media:UniqueID() 105 | if not id then return end 106 | 107 | local query = ("SELECT expired FROM `%s` WHERE id='%s'"):format(TableName, id) 108 | local results = sql.Query(query) 109 | 110 | if istable(results) then -- update 111 | 112 | if MediaPlayer.DEBUG then 113 | print("MediaPlayer.Metadata.Save Results:") 114 | PrintTable(results) 115 | end 116 | 117 | results = results[1] 118 | 119 | local expired = ( tonumber(results.expired) == 1 ) 120 | 121 | if expired then 122 | 123 | -- Update possible new metadata 124 | query = "UPDATE `%s` SET request_count=request_count+1, title=%s, duration=%s, thumbnail=%s, extra=%s, last_request=%s, last_updated=%s, expired=0 WHERE id='%s'" 125 | query = query:format( TableName, 126 | sql.SQLStr( media:Title() ), 127 | media:Duration(), 128 | sql.SQLStr( media:Thumbnail() ), 129 | sql.SQLStr( util.TableToJSON(media._metadata.extra or {}) ), 130 | os.time(), 131 | os.time(), 132 | id ) 133 | 134 | else 135 | 136 | query = "UPDATE `%s` SET request_count=request_count+1, last_request=%s WHERE id='%s'" 137 | query = query:format( TableName, os.time(), id ) 138 | 139 | end 140 | 141 | else -- insert 142 | 143 | query = string.format( "INSERT INTO `%s` ", TableName ) .. 144 | "(id,title,duration,thumbnail,extra,last_request,last_updated) VALUES (" .. 145 | string.format( "'%s',", id ) .. 146 | string.format( "%s,", sql.SQLStr( media:Title() ) ) .. 147 | string.format( "%s,", media:Duration() ) .. 148 | string.format( "%s,", sql.SQLStr( media:Thumbnail() ) ) .. 149 | string.format( "%s,", sql.SQLStr( util.TableToJSON(media._metadata.extra or {}) ) ) .. 150 | string.format( "%d,", os.time() ) .. 151 | string.format( "%d)", os.time() ) 152 | 153 | end 154 | 155 | if MediaPlayer.DEBUG then 156 | print("MediaPlayer.Metadata.Save") 157 | print(query) 158 | end 159 | 160 | results = sql.Query(query) 161 | 162 | if results == false then 163 | ErrorNoHalt("MediaPlayer.Metadata.Save: There was an error executing the SQL query\n") 164 | print(query) 165 | end 166 | 167 | return results 168 | end 169 | 170 | -- Create the SQLite table if it doesn't exist 171 | if not sql.TableExists(TableName) then 172 | Msg("MediaPlayer.Metadata: Creating `" .. TableName .. "` table...\n") 173 | sql.Query(TableStruct) 174 | end 175 | -------------------------------------------------------------------------------- /lua/mediaplayer/sh_services.lua: -------------------------------------------------------------------------------- 1 | MediaPlayer.Services = {} 2 | 3 | function MediaPlayer.RegisterService( service ) 4 | 5 | local base 6 | 7 | if service.Base then 8 | base = MediaPlayer.Services[service.Base] 9 | elseif MediaPlayer.Services.base then 10 | base = MediaPlayer.Services.base 11 | end 12 | 13 | -- Inherit base service 14 | setmetatable( service, { __index = base } ) 15 | 16 | -- Create base class for service 17 | baseclass.Set( "mp_service_" .. service.Id, service ) 18 | 19 | -- Store service 20 | MediaPlayer.Services[ service.Id ] = service 21 | 22 | if MediaPlayer.DEBUG then 23 | print( "MediaPlayer.RegisterService", service.Name ) 24 | end 25 | 26 | end 27 | 28 | function MediaPlayer.GetValidServiceNames( whitelist ) 29 | local tbl = {} 30 | 31 | for _, service in pairs(MediaPlayer.Services) do 32 | if not rawget(service, "Abstract") then 33 | if whitelist then 34 | if table.HasValue( whitelist, service.Id ) then 35 | table.insert( tbl, service.Name ) 36 | end 37 | else 38 | table.insert( tbl, service.Name ) 39 | end 40 | end 41 | end 42 | 43 | return tbl 44 | end 45 | 46 | function MediaPlayer.GetSupportedServiceIDs() 47 | local tbl = {} 48 | 49 | for _, service in pairs(MediaPlayer.Services) do 50 | if not rawget(service, "Abstract") then 51 | table.insert( tbl, service.Id ) 52 | end 53 | end 54 | 55 | return tbl 56 | end 57 | 58 | function MediaPlayer.ValidUrl( url ) 59 | 60 | for id, service in pairs(MediaPlayer.Services) do 61 | if service:Match( url ) then 62 | return true 63 | end 64 | end 65 | 66 | return false 67 | 68 | end 69 | 70 | function MediaPlayer.GetMediaForUrl( url, webpageFallback ) 71 | 72 | local service 73 | 74 | for id, s in pairs(MediaPlayer.Services) do 75 | if s:Match( url ) then 76 | service = s 77 | break 78 | end 79 | end 80 | 81 | if not service then 82 | if webpageFallback then 83 | service = MediaPlayer.Services.www 84 | else 85 | service = MediaPlayer.Services.base 86 | end 87 | end 88 | 89 | return service:New( url ) 90 | 91 | end 92 | 93 | -- Load services 94 | do 95 | local path = "services/" 96 | 97 | local fullpath = "mediaplayer/" .. path 98 | 99 | local services = { 100 | "base", -- MUST LOAD FIRST! 101 | 102 | -- Browser 103 | "browser", -- base 104 | "youtube", 105 | "googledrive", 106 | "twitch", 107 | "twitchstream", 108 | "vimeo", 109 | 110 | -- HTML Resources 111 | "resource", -- base 112 | "image", 113 | "html5_video", 114 | "webpage", 115 | 116 | -- IGModAudioChannel 117 | "audiofile", 118 | "shoutcast", 119 | "soundcloud" 120 | } 121 | 122 | for _, name in ipairs(services) do 123 | local clfile = path .. name .. "/cl_init.lua" 124 | local svfile = path .. name .. "/init.lua" 125 | local shfile = fullpath .. name .. ".lua" 126 | 127 | if file.Exists(shfile, "LUA") then 128 | clfile = shfile 129 | svfile = shfile 130 | end 131 | 132 | SERVICE = {} 133 | 134 | if SERVER then 135 | AddCSLuaFile(clfile) 136 | include(svfile) 137 | else 138 | include(clfile) 139 | end 140 | 141 | MediaPlayer.RegisterService( SERVICE ) 142 | SERVICE = nil 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /lua/mediaplayer/shared.lua: -------------------------------------------------------------------------------- 1 | MediaPlayer = MediaPlayer or {} 2 | MP = MediaPlayer 3 | 4 | include "utils.lua" 5 | include "sh_cvars.lua" 6 | 7 | --[[--------------------------------------------------------- 8 | Config 9 | 10 | Store service API keys, etc. 11 | -----------------------------------------------------------]] 12 | 13 | MediaPlayer.config = {} 14 | 15 | --- 16 | -- Apply configuration values to the mediaplayer config. 17 | -- 18 | -- @param config Table with configuration values. 19 | -- 20 | function MediaPlayer.SetConfig( config ) 21 | table.Merge( MediaPlayer.config, config ) 22 | end 23 | 24 | --- 25 | -- Method for easily grabbing config value without checking that each fragment 26 | -- exists. 27 | -- 28 | -- @param key e.g. "json.key.fragments" 29 | -- 30 | function MediaPlayer.GetConfigValue( key ) 31 | local value = MediaPlayerUtils.TableLookup( MediaPlayer.config, key ) 32 | 33 | if type(value) == 'nil' then 34 | ErrorNoHalt("WARNING: MediaPlayer config value not found for key `" .. tostring(key) .. "`\n") 35 | end 36 | 37 | return value 38 | end 39 | 40 | if SERVER then 41 | AddCSLuaFile "config/client.lua" 42 | include "config/server.lua" 43 | else 44 | include "config/client.lua" 45 | end 46 | 47 | 48 | --[[--------------------------------------------------------- 49 | Shared includes 50 | -----------------------------------------------------------]] 51 | 52 | include "sh_events.lua" 53 | include "sh_mediaplayer.lua" 54 | include "sh_services.lua" 55 | include "sh_history.lua" 56 | include "sh_metadata.lua" 57 | 58 | hook.Add("Initialize", "InitMediaPlayer", function() 59 | hook.Run("InitMediaPlayer", MediaPlayer) 60 | end) 61 | 62 | -- No fun allowed 63 | hook.Add( "CanDrive", "DisableMediaPlayerDriving", function(ply, ent) 64 | if IsValid(ent) and ent.IsMediaPlayerEntity then 65 | return IsValid(ply) and ply:IsAdmin() 66 | end 67 | end) 68 | -------------------------------------------------------------------------------- /lua/mediaplayer/sv_requests.lua: -------------------------------------------------------------------------------- 1 | util.AddNetworkString( "MEDIAPLAYER.RequestListen" ) 2 | util.AddNetworkString( "MEDIAPLAYER.RequestUpdate" ) 3 | util.AddNetworkString( "MEDIAPLAYER.RequestMedia" ) 4 | util.AddNetworkString( "MEDIAPLAYER.RequestPause" ) 5 | util.AddNetworkString( "MEDIAPLAYER.RequestSkip" ) 6 | util.AddNetworkString( "MEDIAPLAYER.RequestSeek" ) 7 | util.AddNetworkString( "MEDIAPLAYER.RequestRemove" ) 8 | util.AddNetworkString( "MEDIAPLAYER.RequestRepeat" ) 9 | util.AddNetworkString( "MEDIAPLAYER.RequestShuffle" ) 10 | util.AddNetworkString( "MEDIAPLAYER.RequestLock" ) 11 | 12 | local REQUEST_DELAY = 0.2 13 | 14 | local function RequestWrapper( func ) 15 | local nextRequest 16 | return function( len, ply ) 17 | if not IsValid(ply) then return end 18 | 19 | if nextRequest and nextRequest > RealTime() then 20 | return 21 | end 22 | 23 | local mpId = net.ReadString() 24 | local mp = MediaPlayer.GetById(mpId) 25 | if not IsValid(mp) then return end 26 | 27 | func( mp, ply ) 28 | 29 | nextRequest = RealTime() + REQUEST_DELAY 30 | end 31 | end 32 | 33 | net.Receive( "MEDIAPLAYER.RequestListen", RequestWrapper(function(mp, ply) 34 | 35 | if MediaPlayer.DEBUG then 36 | print("MEDIAPLAYER.RequestListen:", mpId, ply) 37 | end 38 | 39 | -- TODO: check if listener can actually be a listener 40 | if mp:HasListener(ply) then 41 | mp:RemoveListener(ply) 42 | else 43 | mp:AddListener(ply) 44 | end 45 | 46 | end) ) 47 | 48 | --- 49 | -- Event called when a player requests a media update. This will occur when 50 | -- a client determines it's not synced correctly. 51 | -- 52 | -- @param len Net message length. 53 | -- @param ply Player who sent the net message. 54 | -- 55 | net.Receive( "MEDIAPLAYER.RequestUpdate", RequestWrapper(function(mp, ply) 56 | 57 | if MediaPlayer.DEBUG then 58 | print("MEDIAPLAYER.RequestUpdate:", mpId, ply) 59 | end 60 | 61 | mp:SendMedia( mp:GetMedia(), ply ) 62 | 63 | end) ) 64 | 65 | net.Receive( "MEDIAPLAYER.RequestMedia", RequestWrapper(function(mp, ply) 66 | 67 | local url = net.ReadString() 68 | 69 | if MediaPlayer.DEBUG then 70 | print("MEDIAPLAYER.RequestMedia:", url, mp:GetId(), ply) 71 | end 72 | 73 | local allowWebpage = MediaPlayer.Cvars.AllowWebpages:GetBool() 74 | 75 | -- Validate the URL 76 | if not MediaPlayer.ValidUrl( url ) and not allowWebpage then 77 | mp:NotifyPlayer( ply, "The requested URL was invalid." ) 78 | return 79 | end 80 | 81 | -- Build the media object for the URL 82 | local media = MediaPlayer.GetMediaForUrl( url, allowWebpage ) 83 | media:NetReadRequest() 84 | 85 | mp:RequestMedia( media, ply ) 86 | 87 | end) ) 88 | 89 | net.Receive( "MEDIAPLAYER.RequestPause", RequestWrapper(function(mp, ply) 90 | 91 | if MediaPlayer.DEBUG then 92 | print("MEDIAPLAYER.RequestPause:", mp:GetId(), ply) 93 | end 94 | 95 | mp:RequestPause( ply ) 96 | 97 | end) ) 98 | 99 | net.Receive( "MEDIAPLAYER.RequestSkip", RequestWrapper(function(mp, ply) 100 | 101 | if MediaPlayer.DEBUG then 102 | print("MEDIAPLAYER.RequestSkip:", mp:GetId(), ply) 103 | end 104 | 105 | mp:RequestSkip( ply ) 106 | 107 | end) ) 108 | 109 | net.Receive( "MEDIAPLAYER.RequestSeek", RequestWrapper(function(mp, ply) 110 | 111 | local seekTime = net.ReadInt(32) 112 | 113 | if MediaPlayer.DEBUG then 114 | print("MEDIAPLAYER.RequestSeek:", mp:GetId(), seekTime, ply) 115 | end 116 | 117 | mp:RequestSeek( ply, seekTime ) 118 | 119 | end) ) 120 | 121 | net.Receive( "MEDIAPLAYER.RequestRemove", RequestWrapper(function(mp, ply) 122 | 123 | local mediaUID = net.ReadString() 124 | 125 | if MediaPlayer.DEBUG then 126 | print("MEDIAPLAYER.RequestRemove:", mp:GetId(), mediaUID, ply) 127 | end 128 | 129 | mp:RequestRemove( ply, mediaUID ) 130 | 131 | end) ) 132 | 133 | net.Receive( "MEDIAPLAYER.RequestRepeat", RequestWrapper(function(mp, ply) 134 | 135 | if MediaPlayer.DEBUG then 136 | print("MEDIAPLAYER.RequestRepeat:", mp:GetId(), ply) 137 | end 138 | 139 | mp:RequestRepeat( ply ) 140 | 141 | end) ) 142 | 143 | net.Receive( "MEDIAPLAYER.RequestShuffle", RequestWrapper(function(mp, ply) 144 | 145 | if MediaPlayer.DEBUG then 146 | print("MEDIAPLAYER.RequestShuffle:", mp:GetId(), ply) 147 | end 148 | 149 | mp:RequestShuffle( ply ) 150 | 151 | end) ) 152 | 153 | net.Receive( "MEDIAPLAYER.RequestLock", RequestWrapper(function(mp, ply) 154 | 155 | if MediaPlayer.DEBUG then 156 | print("MEDIAPLAYER.RequestLock:", mp:GetId(), ply) 157 | end 158 | 159 | mp:RequestLock( ply ) 160 | 161 | end) ) 162 | -------------------------------------------------------------------------------- /lua/mp_menu/cl_init.lua: -------------------------------------------------------------------------------- 1 | MP = MP or {} 2 | MP.EVENTS = MP.EVENTS or {} 3 | 4 | MP.EVENTS.UI = { 5 | 6 | --[[-------------------------------------------------------- 7 | Sidebar events 8 | ----------------------------------------------------------]] 9 | 10 | SETUP_SIDEBAR = "mp.events.ui.sidebarChanged", 11 | SETUP_PLAYBACK_PANEL = "mp.events.ui.setupPlaybackPanel", 12 | SETUP_MEDIA_PANEL = "mp.events.ui.setupMediaPanel", 13 | 14 | MEDIA_PLAYER_CHANGED = "mp.events.ui.mediaPlayerChanged", 15 | 16 | OPEN_REQUEST_MENU = "mp.events.ui.openRequestMenu", 17 | FAVORITE_MEDIA = "mp.events.ui.favoriteMedia", 18 | REMOVE_MEDIA = "mp.events.ui.removeMedia", 19 | SKIP_MEDIA = "mp.events.ui.skipMedia", 20 | VOTE_MEDIA = "mp.events.ui.voteMedia", 21 | TOGGLE_LOCK = "mp.events.ui.toggleLock", 22 | TOGGLE_PAUSE = "mp.events.ui.togglePause", 23 | TOGGLE_REPEAT = "mp.events.ui.toggleRepeat", 24 | TOGGLE_SHUFFLE = "mp.events.ui.toggleShuffle", 25 | SEEK = "mp.events.ui.seek", 26 | 27 | START_SEEKING = "mp.events.ui.startSeeking", 28 | STOP_SEEKING = "mp.events.ui.stopSeeking", 29 | 30 | PRIVILEGED_PLAYER = "mp.events.ui.privilegedPlayer" 31 | 32 | } 33 | 34 | include "sidebar.lua" 35 | -------------------------------------------------------------------------------- /lua/mp_menu/horizontal_list.lua: -------------------------------------------------------------------------------- 1 | local PANEL = {} 2 | 3 | function PANEL:Init() 4 | DPanelList.Init( self ) 5 | 6 | self:EnableVerticalScrollbar( false ) 7 | self:EnableHorizontal( true ) 8 | self:SetAutoSize( true ) 9 | end 10 | 11 | function PANEL:Rebuild() 12 | 13 | local OffsetX, OffsetY = 0, 0 14 | self.m_iBuilds = self.m_iBuilds + 1; 15 | 16 | self:CleanList() 17 | 18 | if ( self.Horizontal ) then 19 | 20 | local x, y = self.Padding, self.Padding; 21 | for k, panel in pairs( self.Items ) do 22 | 23 | if ( panel:IsVisible() ) then 24 | 25 | local OwnLine = (panel.m_strLineState and panel.m_strLineState == "ownline"); 26 | 27 | local w = panel:GetWide() 28 | local h = panel:GetTall() 29 | 30 | local breakLine = ( not self.m_bSizeToContents and 31 | ( x > self.Padding ) and 32 | (x + w > self:GetWide() or OwnLine) ) 33 | 34 | if breakLine then 35 | 36 | x = self.Padding 37 | y = y + h + self.Spacing 38 | 39 | end 40 | 41 | if ( self.m_fAnimTime > 0 and self.m_iBuilds > 1 ) then 42 | panel:MoveTo( x, y, self.m_fAnimTime, 0, self.m_fAnimEase ) 43 | else 44 | panel:SetPos( x, y ) 45 | end 46 | 47 | x = x + w + self.Spacing 48 | 49 | OffsetX = x 50 | OffsetY = y + h + self.Spacing 51 | 52 | if ( OwnLine ) then 53 | 54 | x = self.Padding 55 | y = y + h + self.Spacing 56 | 57 | end 58 | 59 | end 60 | 61 | end 62 | 63 | else 64 | 65 | for k, panel in pairs( self.Items ) do 66 | 67 | if ( panel:IsVisible() ) then 68 | 69 | if ( self.m_bNoSizing ) then 70 | panel:SizeToContents() 71 | if ( self.m_fAnimTime > 0 and self.m_iBuilds > 1 ) then 72 | panel:MoveTo( (self:GetCanvas():GetWide() - panel:GetWide()) * 0.5, self.Padding + OffsetY, self.m_fAnimTime, 0, self.m_fAnimEase ) 73 | else 74 | panel:SetPos( (self:GetCanvas():GetWide() - panel:GetWide()) * 0.5, self.Padding + OffsetY ) 75 | end 76 | else 77 | panel:SetSize( self:GetCanvas():GetWide() - self.Padding * 2, panel:GetTall() ) 78 | if ( self.m_fAnimTime > 0 and self.m_iBuilds > 1 ) then 79 | panel:MoveTo( self.Padding, self.Padding + OffsetY, self.m_fAnimTime, self.m_fAnimEase ) 80 | else 81 | panel:SetPos( self.Padding, self.Padding + OffsetY ) 82 | end 83 | end 84 | 85 | -- Changing the width might ultimately change the height 86 | -- So give the panel a chance to change its height now, 87 | -- so when we call GetTall below the height will be correct. 88 | -- True means layout now. 89 | panel:InvalidateLayout( true ) 90 | 91 | OffsetY = OffsetY + panel:GetTall() + self.Spacing 92 | 93 | end 94 | 95 | end 96 | 97 | OffsetY = OffsetY + self.Padding 98 | 99 | end 100 | 101 | self:GetCanvas():SetWide( OffsetX + self.Padding - self.Spacing ) 102 | self:GetCanvas():SetTall( OffsetY + self.Padding - self.Spacing ) 103 | 104 | -- Although this behaviour isn't exactly implied, center vertically too 105 | if ( self.m_bNoSizing and self:GetCanvas():GetTall() < self:GetTall() ) then 106 | self:GetCanvas():SetPos( 0, (self:GetTall()-self:GetCanvas():GetTall()) * 0.5 ) 107 | end 108 | 109 | end 110 | 111 | function PANEL:PerformLayout() 112 | 113 | local Wide = self:GetWide() 114 | local YPos = 0 115 | 116 | self:Rebuild() 117 | 118 | if self.VBar and not m_bSizeToContents then 119 | 120 | self.VBar:SetPos( self:GetWide() - 13, 0 ) 121 | self.VBar:SetSize( 13, self:GetTall() ) 122 | self.VBar:SetUp( self:GetTall(), self.pnlCanvas:GetTall() ) 123 | YPos = self.VBar:GetOffset() 124 | 125 | if ( self.VBar.Enabled ) then Wide = Wide - 13 end 126 | 127 | end 128 | 129 | if self:GetAutoSize() then 130 | 131 | self:SetWide( self.pnlCanvas:GetWide() ) 132 | self:SetTall( self.pnlCanvas:GetTall() ) 133 | self.pnlCanvas:SetPos( 0, 0 ) 134 | 135 | else 136 | 137 | self.pnlCanvas:SetPos( 0, YPos ) 138 | self.pnlCanvas:SetWide( Wide ) 139 | 140 | end 141 | 142 | end 143 | 144 | derma.DefineControl( "DHorizontalList", "", PANEL, "DPanelList" ) 145 | -------------------------------------------------------------------------------- /lua/mp_menu/init.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile "common.lua" 2 | AddCSLuaFile "sidebar.lua" 3 | AddCSLuaFile "sidebar_tabs.lua" 4 | AddCSLuaFile "volume_control.lua" 5 | AddCSLuaFile "playback.lua" 6 | AddCSLuaFile "queue.lua" 7 | AddCSLuaFile "horizontal_list.lua" 8 | AddCSLuaFile "icons.lua" 9 | -------------------------------------------------------------------------------- /lua/mp_menu/volume_control.lua: -------------------------------------------------------------------------------- 1 | local math = math 2 | local ceil = math.ceil 3 | local clamp = math.Clamp 4 | 5 | local surface = surface 6 | local color_white = color_white 7 | 8 | local PANEL = {} 9 | 10 | PANEL.Margin = 16 11 | PANEL.ButtonWidth = 18 12 | PANEL.ButtonSpacing = 8 13 | PANEL.BackgroundColor = Color( 28, 100, 157 ) 14 | 15 | function PANEL:Init() 16 | 17 | self.BaseClass.Init( self ) 18 | 19 | self.VolumeButton = vgui.Create( "MP.VolumeButton", self ) 20 | 21 | self.VolumeSlider = vgui.Create( "MP.VolumeSlider", self ) 22 | 23 | self.BtnList = vgui.Create( "DHorizontalList", self ) 24 | self.BtnList:SetSpacing( self.ButtonSpacing ) 25 | 26 | if hook.Run( MP.EVENTS.UI.PRIVILEGED_PLAYER ) then 27 | self.RepeatBtn = vgui.Create( "MP.RepeatButton" ) 28 | self:AddButton( self.RepeatBtn ) 29 | self.ShuffleBtn = vgui.Create( "MP.ShuffleButton" ) 30 | self:AddButton( self.ShuffleBtn ) 31 | self.LockBtn = vgui.Create( "MP.LockButton" ) 32 | self:AddButton( self.LockBtn ) 33 | end 34 | 35 | self:OnVolumeChanged( MediaPlayer.Volume() ) 36 | 37 | hook.Add( MP.EVENTS.VOLUME_CHANGED, self, self.OnVolumeChanged ) 38 | hook.Add( MP.EVENTS.UI.MEDIA_PLAYER_CHANGED, self, self.OnMediaPlayerChanged ) 39 | 40 | end 41 | 42 | function PANEL:AddButton( panel ) 43 | self.BtnList:AddItem( panel ) 44 | end 45 | 46 | function PANEL:OnVolumeChanged( volume ) 47 | 48 | self.VolumeSlider:SetSlideX( volume ) 49 | 50 | self:InvalidateChildren() 51 | 52 | end 53 | 54 | function PANEL:OnMediaPlayerChanged( mp ) 55 | 56 | if hook.Run( MP.EVENTS.UI.PRIVILEGED_PLAYER ) then 57 | self.RepeatBtn:SetEnabled( mp:GetQueueRepeat() ) 58 | self.ShuffleBtn:SetEnabled( mp:GetQueueShuffle() ) 59 | self.LockBtn:SetEnabled( mp:GetQueueLocked() ) 60 | end 61 | 62 | end 63 | 64 | function PANEL:Paint( w, h ) 65 | 66 | surface.SetDrawColor( self.BackgroundColor ) 67 | surface.DrawRect( 0, 0, w, h ) 68 | 69 | end 70 | 71 | function PANEL:PerformLayout( w, h ) 72 | 73 | self.BtnList:InvalidateLayout( true ) 74 | self.BtnList:CenterVertical() 75 | self.BtnList:AlignRight( self.Margin ) 76 | 77 | self.VolumeButton:CenterVertical() 78 | self.VolumeButton:AlignLeft( self.Margin ) 79 | 80 | local sliderWidth = ( self.BtnList:GetPos() - 15 ) - 81 | ( self.VolumeButton:GetPos() + self.VolumeButton:GetWide() + 15 ) 82 | self.VolumeSlider:SetWide( sliderWidth ) 83 | self.VolumeSlider:CenterVertical() 84 | self.VolumeSlider:MoveRightOf( self.VolumeButton, 15 ) 85 | 86 | end 87 | 88 | function PANEL:OnRemove() 89 | 90 | hook.Remove( MP.EVENTS.VOLUME_CHANGED, self ) 91 | 92 | end 93 | 94 | derma.DefineControl( "MP.VolumeControl", "", PANEL, "DPanel" ) 95 | 96 | 97 | local VOLUME_BUTTON = {} 98 | 99 | function VOLUME_BUTTON:Init() 100 | 101 | self.BaseClass.Init( self ) 102 | 103 | self:SetIcon( 'mp-volume' ) 104 | self:SetSize( 18, 17 ) 105 | 106 | end 107 | 108 | function VOLUME_BUTTON:DoClick() 109 | 110 | MediaPlayer.ToggleMute() 111 | 112 | end 113 | 114 | derma.DefineControl( "MP.VolumeButton", "", VOLUME_BUTTON, "MP.SidebarButton" ) 115 | 116 | 117 | local VOLUME_SLIDER = {} 118 | 119 | VOLUME_SLIDER.BarHeight = 3 120 | VOLUME_SLIDER.KnobSize = 12 121 | 122 | VOLUME_SLIDER.BarBgColor = Color( 13, 41, 62 ) 123 | 124 | VOLUME_SLIDER.ScrollIncrement = 0.1 -- out of 1 125 | 126 | function VOLUME_SLIDER:Init() 127 | 128 | self.BaseClass.Init( self ) 129 | 130 | self.Knob:SetSize( self.KnobSize, self.KnobSize ) 131 | self.Knob.Paint = self.PaintKnob 132 | 133 | -- Remove some hidden panel child from the inherited DSlider control; I have 134 | -- no idea where it's being created... 135 | for _, child in pairs( self:GetChildren() ) do 136 | if child ~= self.Knob then 137 | child:Remove() 138 | end 139 | end 140 | 141 | end 142 | 143 | function VOLUME_SLIDER:Paint( w, h ) 144 | 145 | local progress = self.m_fSlideX 146 | local vmid = ceil((h / 2) - (self.BarHeight / 2)) 147 | 148 | surface.SetDrawColor( self.BarBgColor ) 149 | surface.DrawRect( 0, vmid, w, self.BarHeight ) 150 | 151 | surface.SetDrawColor( color_white ) 152 | surface.DrawRect( 0, vmid, ceil(w * progress), self.BarHeight ) 153 | 154 | end 155 | 156 | function VOLUME_SLIDER:PaintKnob( w, h ) 157 | 158 | draw.RoundedBoxEx( ceil(w/2), 0, 0, w, h, color_white, true, true, true, true ) 159 | 160 | end 161 | 162 | function VOLUME_SLIDER:SetSlideX( value ) 163 | 164 | if self._lockVolume then return end 165 | 166 | value = clamp(value, 0, 1) 167 | 168 | self.m_fSlideX = value 169 | self:InvalidateLayout() 170 | 171 | self._lockVolume = true 172 | MediaPlayer.Volume( value ) 173 | self._lockVolume = nil 174 | 175 | end 176 | 177 | function VOLUME_SLIDER:OnMouseWheeled( delta ) 178 | 179 | local change = self.ScrollIncrement * delta 180 | local value = clamp(self.m_fSlideX + change, 0, 1) 181 | 182 | self:SetSlideX( value ) 183 | 184 | end 185 | 186 | derma.DefineControl( "MP.VolumeSlider", "", VOLUME_SLIDER, "DSlider" ) 187 | 188 | 189 | local REPEAT_BTN = {} 190 | 191 | function REPEAT_BTN:Init() 192 | self.BaseClass.Init( self ) 193 | self:SetIcon( "mp-repeat" ) 194 | self:SetTooltip( "Repeat" ) 195 | end 196 | 197 | function REPEAT_BTN:DoClick() 198 | self.BaseClass.DoClick( self ) 199 | hook.Run( MP.EVENTS.UI.TOGGLE_REPEAT ) 200 | end 201 | 202 | derma.DefineControl( "MP.RepeatButton", "", REPEAT_BTN, "MP.SidebarToggleButton" ) 203 | 204 | 205 | local SHUFFLE_BTN = {} 206 | 207 | function SHUFFLE_BTN:Init() 208 | self.BaseClass.Init( self ) 209 | self:SetIcon( "mp-shuffle" ) 210 | self:SetTooltip( "Shuffle" ) 211 | end 212 | 213 | function SHUFFLE_BTN:DoClick() 214 | self.BaseClass.DoClick( self ) 215 | hook.Run( MP.EVENTS.UI.TOGGLE_SHUFFLE ) 216 | end 217 | 218 | derma.DefineControl( "MP.ShuffleButton", "", SHUFFLE_BTN, "MP.SidebarToggleButton" ) 219 | 220 | 221 | local LOCK_BTN = {} 222 | 223 | function LOCK_BTN:Init() 224 | self.BaseClass.Init( self ) 225 | self:SetIcon( "mp-lock-open" ) 226 | self:SetTooltip( "Toggle Queue Lock" ) 227 | end 228 | 229 | function LOCK_BTN:DoClick() 230 | self.BaseClass.DoClick( self ) 231 | 232 | hook.Run( MP.EVENTS.UI.TOGGLE_LOCK ) 233 | self:UpdateIcon() 234 | end 235 | 236 | function LOCK_BTN:SetEnabled( bEnabled ) 237 | self.BaseClass.SetEnabled( self, bEnabled ) 238 | self:UpdateIcon() 239 | end 240 | 241 | function LOCK_BTN:UpdateIcon() 242 | local icon = self:GetEnabled() and "mp-lock" or "mp-lock-open" 243 | self:SetIcon( icon ) 244 | end 245 | 246 | derma.DefineControl( "MP.LockButton", "", LOCK_BTN, "MP.SidebarToggleButton" ) 247 | -------------------------------------------------------------------------------- /materials/entities/mediaplayer_tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/materials/entities/mediaplayer_tv.png -------------------------------------------------------------------------------- /materials/mediaplayer/ui/spritesheet2015-10-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/materials/mediaplayer/ui/spritesheet2015-10-7.png -------------------------------------------------------------------------------- /materials/models/gmod_tower/suitetv_large.vmt: -------------------------------------------------------------------------------- 1 | "VertexlitGeneric" 2 | { 3 | "$basetexture" "models/gmod_tower/suitetv_large" 4 | } 5 | -------------------------------------------------------------------------------- /materials/models/gmod_tower/suitetv_large.vtf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/materials/models/gmod_tower/suitetv_large.vtf -------------------------------------------------------------------------------- /materials/theater/STATIC.vmt: -------------------------------------------------------------------------------- 1 | "UnlitGeneric" 2 | { 3 | "$basetexture" "theater/STATIC" 4 | "$surfaceprop" "glass" 5 | "%keywords" "theater" 6 | "Proxies" 7 | { 8 | "AnimatedTexture" 9 | { 10 | "animatedTextureVar" "$basetexture" 11 | "animatedTextureFrameNumVar" "$frame" 12 | "animatedTextureFrameRate" "16" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /materials/theater/STATIC.vtf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/materials/theater/STATIC.vtf -------------------------------------------------------------------------------- /mediaplayer.fgd: -------------------------------------------------------------------------------- 1 | @PointClass base(Targetname, Origin, Angles) studioprop() = mediaplayer_tv : "Plays media" 2 | [ 3 | output OnMediaStarted(void) : "Fired when media playback begins" 4 | output OnQueueEmpty(void) : "Fired when all queued media finishes playing" 5 | 6 | input AddPlayer(void) : "Adds the activator to the list of media listeners" 7 | input RemovePlayer(void) : "Removes the activator from the list of media listeners" 8 | input RemoveAllPlayers(void) : "Removes all players from the list of media listeners" 9 | input PlayPauseMedia(void) : "Toggles the play or pause state of the current media" 10 | input ClearMedia(void) : "Clears all queued and active media" 11 | ] 12 | -------------------------------------------------------------------------------- /mediaplayer.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /models/gmod_tower/suitetv_large.dx80.vtx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/models/gmod_tower/suitetv_large.dx80.vtx -------------------------------------------------------------------------------- /models/gmod_tower/suitetv_large.dx90.vtx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/models/gmod_tower/suitetv_large.dx90.vtx -------------------------------------------------------------------------------- /models/gmod_tower/suitetv_large.mdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/models/gmod_tower/suitetv_large.mdl -------------------------------------------------------------------------------- /models/gmod_tower/suitetv_large.phy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/models/gmod_tower/suitetv_large.phy -------------------------------------------------------------------------------- /models/gmod_tower/suitetv_large.sw.vtx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/models/gmod_tower/suitetv_large.sw.vtx -------------------------------------------------------------------------------- /models/gmod_tower/suitetv_large.vvd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/models/gmod_tower/suitetv_large.vvd -------------------------------------------------------------------------------- /resource/fonts/ClearSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmaddock/gm-mediaplayer/b7854a3dd9f6c005c4ace41f973bf21012bdbaa9/resource/fonts/ClearSans-Medium.ttf --------------------------------------------------------------------------------