├── .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 | 
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 | [](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 |
--------------------------------------------------------------------------------
/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 |
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-%w->%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
--------------------------------------------------------------------------------