Plugin Directory

Changeset 3408984


Ignore:
Timestamp:
12/03/2025 07:53:11 AM (4 months ago)
Author:
wpstream
Message:

Update to version 4.9.4 from GitHub

Location:
wpstream
Files:
22 edited
1 copied

Legend:

Unmodified
Added
Removed
  • wpstream/tags/4.9.4/admin/js/wpstream-onboarding-page.js

    r3394809 r3408984  
    88 */
    99function wpstream_track_onboarding_step(action, step, element_type= 'button', element_name = '') {
     10    console.log('Tracking onboarding step:', action, step, element_type, element_name);
    1011    fetch( wpstream_onboarding_page_vars.request_url + '/onboarding/index.php', {
    1112        method: 'POST',
     
    3031
    3132window.addEventListener('DOMContentLoaded', async function() {
    32     if ( jQuery('#wpstream_have_token').length >0 ) {
    33         wpstream_track_onboarding_step('onboarding_loaded', 'wpstream_step_2');
    34     } else {
    35         wpstream_track_onboarding_step('onboarding_loaded', 'login_step');
     33    // if it's the create channel page
     34    if ( wpstream_onboarding_page_vars.current_page === 'post_edit' ) {
     35        wpstream_track_onboarding_step('onboarding_loaded', 'create_channel_step');
     36    }
     37
     38    // if it's the WpStream -> Quick start page
     39    if ( wpstream_onboarding_page_vars.current_page === 'onboarding' ) {
     40        if (jQuery('#wpstream_have_token').length > 0) {
     41            wpstream_track_onboarding_step('onboarding_loaded', 'wpstream_step_2');
     42        } else {
     43            wpstream_track_onboarding_step('onboarding_loaded', 'register_step');
     44        }
    3645    }
    3746});
  • wpstream/tags/4.9.4/includes/class-wpstream-player.php

    r3404022 r3408984  
    479479                $player_logo_horizontal_position = 'logo-' . explode( '-', $player_logo_position )[1];
    480480            }
    481                 echo'
    482                 <video id="wpstream-video'.$now.'"     '.$poster_data.'  class="video-js vjs-default-skin  vjs-fluid vjs-wpstream ' . esc_attr($has_trailer_class) . ' ' . $player_theme . ' ' . $player_logo_position_class . ' ' . $player_logo_horizontal_position . '" playsinline="true" '.$is_muted_str." ".$autoplay_str.'>
    483                
     481//          echo'
     482//              <div class="wpstream-video-container">
     483//                  <div id="wpstream-pre-load-spinner" class="wpstream-pre-load-spinner"></div>
     484//                  <video id="wpstream-video'.$now.'"     '.$poster_data.'  class="video-js vjs-default-skin  vjs-fluid vjs-wpstream ' . esc_attr($has_trailer_class) . ' ' . $player_theme . ' ' . $player_logo_position_class . ' ' . $player_logo_horizontal_position . '" playsinline="true" '.$is_muted_str." ".$autoplay_str.'>
     485//                  </video>
     486//              </div>';
     487            echo '<div class="wpstream-pre-load-spinner"></div>';
     488            echo'
     489                    <video id="wpstream-video'.$now.'"     '.$poster_data.'  class="video-js vjs-default-skin  vjs-fluid vjs-wpstream ' . esc_attr($has_trailer_class) . ' ' . $player_theme . ' ' . $player_logo_position_class . ' ' . $player_logo_horizontal_position . '" playsinline="true" '.$is_muted_str." ".$autoplay_str.'>
     490                   
    484491                </video>';
    485492                if ($video_trailer){
     
    975982                                unmuteTrailerButtonElementId: "wpstream_video_on_demand_unmute_trailer_btn_'.$now.'",
    976983                                playVideoButtonElementId: "wpstream_video_on_demand_play_video_btn_'.$now.'",
    977 //                                playerLogoSettings: {
    978 //                                    image: "'. $this->wpstream_get_video_player_logo( $product_id ) . '",
    979 //                                    position: "' . esc_html( get_option('wpstream_player_logo_position','top-right') ) . '",
    980 //                                    opacity: ' . ( intval ( esc_html( get_option('wpstream_player_logo_opacity','100') ) ) / 100 ) . ',
    981 //                                    width: 100,
    982 //                                    height: "auto",
    983 //                                    padding: 10,
    984 //                                },
     984                                playerLogoSettings: {
     985                                    image: "'. $this->wpstream_get_video_player_logo( $product_id ) . '",
     986                                    position: "' . esc_html( get_option('wpstream_player_logo_position','top-right') ) . '",
     987                                    opacity: ' . ( intval ( esc_html( get_option('wpstream_player_logo_opacity','100') ) ) / 100 ) . ',
     988                                    width: 100,
     989                                    height: "auto",
     990                                    padding: 10,
     991                                },
    985992                            });
    986993                        });
     
    10021009                                autoplay: '.var_export($autoplay, true).',
    10031010                                muted: '.var_export($muted, true).',
    1004 //                                playerLogoSettings: {
    1005 //                                    image: "'. $this->wpstream_get_video_player_logo( $product_id ) . '",
    1006 //                                    position: "' . esc_html( get_option('wpstream_player_logo_position','top-right') ) . '",
    1007 //                                    opacity: ' . ( intval ( esc_html( get_option('wpstream_player_logo_opacity','100') ) ) / 100 ) . ',
    1008 //                                    width: 100,
    1009 //                                    height: "auto",
    1010 //                                    padding: 10,
    1011 //                                }
     1011                                playerLogoSettings: {
     1012                                    image: "'. $this->wpstream_get_video_player_logo( $product_id ) . '",
     1013                                    position: "' . esc_html( get_option('wpstream_player_logo_position','top-right') ) . '",
     1014                                    opacity: ' . ( intval ( esc_html( get_option('wpstream_player_logo_opacity','100') ) ) / 100 ) . ',
     1015                                    width: 100,
     1016                                    height: "auto",
     1017                                    padding: 10,
     1018                                }
    10121019                            });
    10131020                        });
  • wpstream/tags/4.9.4/public/class-wpstream-public.php

    r3390939 r3408984  
    17191719               
    17201720                    if ( false === $event_list_paid_posts ) {
    1721                         print '  transient paid expired '; 
    17221721                        $args = array(
    17231722                            'posts_per_page'    => -1,
  • wpstream/tags/4.9.4/public/css/broadcaster.css

    r3362236 r3408984  
    8181    left: 1.5em;
    8282    opacity: 90%;
     83    z-index: 3;
    8384}
    8485
     
    124125
    125126#localVideo {
    126     width: 100%;
    127     height: 100%;
     127    position: relative;
     128    z-index: 2;
     129    width: 100%;
    128130    background-color: #EEEEEE;
    129131}
     
    300302}
    301303
     304.info-message,
     305.error-message {
     306    position: relative;
     307    padding: 1rem;
     308    padding-right: 2.5rem; /* Make space for the close button */
     309}
     310
     311.error-message .dismiss-message {
     312    position: absolute;
     313    top: 50%;
     314    right: 0.5rem;
     315    transform: translateY(-50%);
     316    background: none;
     317    border: none;
     318    font-size: 1.5rem;
     319    line-height: 1;
     320    color: inherit;
     321    cursor: pointer;
     322    opacity: 0.7;
     323    padding: 0.5rem;
     324}
     325
     326.error-message .dismiss-message:hover {
     327    opacity: 1;
     328}
     329
     330
    302331.success-message {
    303332    color: #00a32a;
  • wpstream/tags/4.9.4/public/css/wpstream_style.css

    r3402264 r3408984  
    8484    top: 50%;
    8585    left: 50%;
    86     width: 30px;
    87     height: 30px;
     86    width: 40px;
     87    height: 40px;
    8888    margin: -25px 0 0 -25px;
    8989    opacity: 0.85;
    90     border-radius: 25px;
     90    border-radius: 40px;
    9191    border: 6px solid rgba(43, 51, 63, 0.7);
    9292    border-top-color: white;
    9393    animation: wpstream-spinner-spin 1.1s linear infinite;
    94     z-index: 1000;
     94    z-index: 1;
    9595}
    9696
     
    168168    position:relative;
    169169    height: 30px;
    170     position: relative;
    171170    background: transparent;
    172171    margin-bottom: 15px;
  • wpstream/tags/4.9.4/public/js/broadcaster.js

    r3364177 r3408984  
    5151        vga: {
    5252            width: { ideal: 640 },
    53             height: { ideal: 480 },
     53            height: { ideal: 360 },
    5454        },
    5555        hd: {
    56             width: { ideal: 1280 },
    57             height: { ideal: 720 },
     56            width: { exact: 1280 },
     57            height: { exact: 720 },
    5858        },
    5959        fhd: {
    60             width: { ideal: 1920 },
    61             height: { ideal: 1080 },
     60            width: { exact: 1920 },
     61            height: { exact: 1080 },
    6262        },
    6363        square: {
    64             width: { ideal: 800 },
    65             height: { ideal: 600 },
     64            width: { exact: 800 },
     65            height: { exact: 600 },
    6666        },
    6767        default: {
    68             width: { ideal: 1280 },
    69             height: { ideal: 720 },
     68            width: { min: 640, ideal: 1280, max: 1920 },
     69            height: { min: 360, ideal: 720, max: 1080 },
    7070        },
    7171    };
    7272
    7373    const displayResolutions = {
    74         vga: { width: 640, height: 480 },
     74        vga: { width: 640, height: 360 },
    7575        hd: { width: 1280, height: 720 },
    7676        fhd: { width: 1920, height: 1080 },
     
    8989            console.log(
    9090                "Resolution: " +
    91                     videoElement.videoWidth +
    92                     "x" +
    93                     videoElement.videoHeight
     91                videoElement.videoWidth +
     92                "x" +
     93                videoElement.videoHeight
    9494            );
    9595
     
    201201        messageElement.textContent = message;
    202202
     203        const dismissButton = document.createElement("button");
     204        dismissButton.className = 'dismiss-message';
     205        dismissButton.innerHTML = '&times;';
     206        dismissButton.addEventListener('click', function() {
     207            messageElement.remove();
     208        });
     209
     210        messageElement.appendChild(dismissButton);
     211
    203212        messageContainer.innerHTML = "";
    204213        messageContainer.appendChild(messageElement);
     
    217226            "connected",
    218227            "disconnected",
    219             "connecting"
     228            "connecting",
     229            "reconnecting"
    220230        );
    221231
     
    231241                statusText.textContent = "Connecting...";
    232242                liveIndicatorError.style.display = 'inline';
    233                 liveIndicatorLive.innerContent = 'Connecting';
     243                liveIndicatorError.innerText = 'Connecting...';
    234244                break;
    235245            case "reconnecting":
    236246                statusIndicator.classList.add("connecting");
    237                 statusText.textContent = "Reconnecting...";
     247                statusText.textContent = "Reconnecting";
    238248                liveIndicatorError.style.display = 'inline';
    239                 liveIndicatorLive.innerContent = 'Reconnecting';
     249                liveIndicatorLive.style.display = 'none';
     250                liveIndicatorError.innerText = 'Connection lost. Reconnecting in 10 seconds...';
    240251                break;
    241252            case "disconnected":
     
    249260    }
    250261
    251     function createInput( shouldAutoStart = false ) {
     262    function createInput( shouldAutoStart = false, keepMessages = false ) {
    252263        if (streamingButton) {
    253264            streamingButton.disabled = true;
     
    259270        }
    260271
    261         resetMessages();
     272        if ( !keepMessages ) {
     273            resetMessages();
     274        }
     275
     276        if (localStream) {
     277            localStream.getTracks().forEach((track) => track.stop());
     278            localStream = null;
     279        }
    262280
    263281        input = OvenLiveKit.create({
    264282            callbacks: {
    265283                error: function (error) {
    266                     let errorMessage = "";
    267 
    268                     if (error.message) {
    269                         errorMessage = error.message;
    270                     } else if (error.name) {
    271                         errorMessage = error.name;
    272                     } else {
    273                         errorMessage = error.toString();
     284                    let errorMessage = '';
     285
     286                        if (error.message) {
     287                            errorMessage = error.message;
     288                        } else if (error.name) {
     289                            errorMessage = error.name;
     290                        } else {
     291                            errorMessage = error.toString();
     292                        }
     293
     294                    if (error.name === "OverconstrainedError") {
     295                        showMessage(
     296                            "Your browser or camera does not support this frame size: " + videoResolutionSelect.value,
     297                            'error'
     298                        );
     299                        videoResolutionSelect.value = 'default';
     300                        createInput(shouldAutoStart, true);
     301                        return;
    274302                    }
    275303
    276                     if (errorMessage === "OverconstrainedError") {
    277                         errorMessage =
    278                             "The input device does not support the specified resolution or frame rate.";
    279                     }
    280 
    281                     resetMessages();
    282                     showMessage(errorMessage, "error");
    283 
    284                     if ( shouldAutoStart) {
    285                         considerReconnect = false;
    286                     }
    287                 },
    288                 connectionClosed: function (type, event) {
    289                     console.log("Connection closed:", type, event);
    290                     streamingStarted = false;
    291                     updateStatus("disconnected");
    292 
    293                     if (streamingButton) {
    294                         streamingButton.classList.remove("hidden");
    295                         streamingButton.disabled = false;
    296                     }
    297                     if (stopButton) {
    298                         stopButton.classList.add("hidden");
    299                     }
     304                        resetMessages();
     305                        showMessage(errorMessage, "error");
     306
     307                        if (shouldAutoStart) {
     308                            considerReconnect = false;
     309                        }
     310                    },
     311                    connectionClosed: function (type, event) {
     312                        console.log("Connection closed:", type, event);
     313                        streamingStarted = false;
     314                        // updateStatus("disconnected");
     315
     316                        // if (streamingButton) {
     317                        //  streamingButton.classList.remove("hidden");
     318                        //  streamingButton.disabled = false;
     319                        // }
     320                        // if (stopButton) {
     321                        //  stopButton.classList.add("hidden");
     322                        // }
    300323
    301324                    if (considerReconnect && !pendingReconnect) {
     
    307330                    } else {
    308331                        console.log('connection closed, not reconnecting');
    309                         updateInputState(false);
     332                        // updateInputState(false);
    310333                    }
    311334                },
     
    314337                    if ( state === 'connected' ) {
    315338                        // showMessage("Broadcast started successfully");
     339                        updateStatus('connected');
    316340                    }
    317341
    318                     if (state === "disconnected" && considerReconnect) {
    319                         streamingStarted = false;
    320                         updateStatus("disconnected");
    321 
    322                         if (considerReconnect && !pendingReconnect) {
    323                             console.log('connection closed, attempting to reconnect from ice state change');
    324                             // showMessage("Connection lost, attempting to reconnect...", "info");
    325                             attemptReconnect();
    326                         } else {
    327                             showMessage(
    328                                 "Connection failed. Please check your network settings.",
    329                                 "error"
    330                             );
    331                         }
    332                     }
     342                        if (state === "disconnected" && considerReconnect) {
     343                            streamingStarted = false;
     344                            if (considerReconnect && !pendingReconnect) {
     345                                console.log(
     346                                    "connection closed, attempting to reconnect from ice state change"
     347                                );
     348                                updateStatus("reconnecting");
     349                                attemptReconnect();
     350                            } else {
     351                                showMessage(
     352                                    "Connection failed. Please check your network settings.",
     353                                    "error"
     354                                );
     355                            }
     356                        }
     357                    },
    333358                },
    334             },
    335         });
    336 
    337         input.attachMedia(videoElement);
    338 
    339         if (videoSourceSelect.value) {
     359            });
     360
     361            input.attachMedia(videoElement);
     362
     363        if (videoSourceSelect.options.length > 0) {
    340364            if (videoSourceSelect.value === "displayCapture") {
    341365                input
     
    445469    function attemptReconnect() {
    446470        console.log("attemptReconnect()");
     471        updateStatus('reconnecting');
    447472
    448473        if ( pendingReconnect ) {
     
    451476        }
    452477
    453         considerReconnect = false;
    454         pendingReconnect = true;
    455 
    456         if ( input ) {
    457             input.callbacks = {
    458                 error: function() {},
    459                 connectionClosed: function () {},
    460                 iceStateChange: function () {},
    461             };
    462 
    463             if( input.streamingMode === 'whip') {
    464                 input.stopStreaming();
    465             } else if ( input.streamingMode === 'webrtc' ) {
    466                 if( input.webSocket ) {
     478        // Clean up existing connection before reconnecting
     479        if (input) {
     480            if (input.peerConnection || input.webSocket) {
     481                // Force cleanup without triggering callbacks
     482                if (input.peerConnection) {
     483                    input.peerConnection.close();
     484                    input.peerConnection = null;
     485                }
     486                if (input.webSocket) {
    467487                    input.webSocket.close();
    468488                    input.webSocket = null;
    469489                }
    470                 if( input.peerConnection ) {
    471                     input.peerConnection.close();
    472                     input.peerConnection = null;
    473                 }
    474             }
    475         }
     490                // Reset streaming mode
     491                input.streamingMode = null;
     492            }
     493        }
     494
     495        pendingReconnect = true;
    476496
    477497        // Show a reconnecting state and allow user to cancel via Stop button
    478         updateStatus("reconnecting");
    479         updateInputState(true);
    480 
     498        // showMessage("Disconnected. Reconnecting in 5 seconds...", "info");
     499        // updateInputState(false);
     500
     501        pendingReconnect = true;
    481502        pendingReconnectTimeout = setTimeout(function () {
    482             if (!considerReconnect && !pendingReconnect) {
    483                 return; // User cancelled or stopped
    484             }
    485 
    486             checkChannelStatus(wpstream_broadcaster_vars.channel_id)
    487                 .then(function(channelActive) {
    488                     if (channelActive) {
    489                         console.log("Channel is active, proceeding with reconnect...");
    490                         // Reset flags before recreating
    491                         pendingReconnect = false;
    492                         pendingReconnectTimeout = null;
    493                         considerReconnect = true;
    494 
    495                         // Create new input and start streaming
    496                         createInput(true);
    497                     } else {
    498                         console.log("Channel is not active, cannot reconnect.");
    499                         pendingReconnect = false;
    500                         pendingReconnectTimeout = null;
    501                         showMessage('Channel is no longer active. Broadcasting stopped', 'error');
    502                         resetStreamingUI();
    503                     }
    504                 })
    505                 .catch(function (error) {
    506                     console.error('Error during reconnect attempt:', error);
    507                     pendingReconnect = false;
    508                     pendingReconnectTimeout = null;
    509                     // Don't attempt another reconnect immediately to avoid loops
    510                     setTimeout(() => {
     503            pendingReconnect = false;
     504            pendingReconnectTimeout = null;
     505            if (considerReconnect) {
     506                checkChannelStatus(wpstream_broadcaster_vars.channel_id)
     507                    .then(function(channelActive) {
     508                        if (channelActive && considerReconnect) {
     509                            console.log("Channel is active, proceeding with reconnect...");
     510                            updateStatus("connecting");
     511                            // input.stopStreaming();
     512                            setTimeout(function () {
     513                                createInput(true);
     514                            }, 5000);
     515                        } else {
     516                            console.log("Channel is not active, cannot reconnect.");
     517                            considerReconnect = false;
     518                            showMessage('Channel is no longer active. Broadcasting stopped');
     519                            resetStreamingUI();
     520                        }
     521                    })
     522                    .catch(function (error) {
     523                        console.error('Error checking channel status');
    511524                        if (considerReconnect) {
     525                            console.error('Error during reconnect attempt:', error);
    512526                            attemptReconnect();
    513527                        }
    514                     }, 5000);
    515                 });
     528                    });
     529
     530                // console.log('Reconnecting...');
     531                // createInput( true );
     532                // startStreaming();
     533            }
    516534        }, reconnectDelayMs);
    517535    }
     
    539557            });
    540558
    541             showMessage(enabled ? "Video enabled" : "Video disabled", "info");
     559            // showMessage(enabled ? "Video enabled" : "Video disabled", "info");
    542560        }
    543561    }
     
    714732                        const parsedResponse = JSON.parse(response);
    715733                        if (parsedResponse.status === 'active') {
     734                            messageContainer.innerHTML = "";
    716735                            resolve(true);
    717736                        } else {
     
    727746                error: function(xhr, status, error) {
    728747                    console.error('Error checking channel status:', error);
     748                    resetStreamingUI();
    729749                    showMessage('Error checking channel status: ' + error, 'error');
    730750                    reject(false);
     
    751771                        const parsedResponse = JSON.parse(response);
    752772                        if (parsedResponse.available_data_mb > 0) {
     773                            messageContainer.innerHTML = '';
    753774                            resolve(true);
    754775                        } else {
     
    801822            input.startStreaming(whipUrl, connectionConfig);
    802823            if ( input ) {
     824                // TODO: check why input is sometimes null here
    803825                console.log('something was wrong' );
    804826            }
    805             updateStatus("connected");
     827            // updateStatus("connected");
    806828            if ( isReconnect ) {
    807829                console.log('Reconnected successfully!');
  • wpstream/tags/4.9.4/public/js/start_streaming.js

    r3402264 r3408984  
    759759        var channelId      = jQuery(this).closest('.event_list_unit').data('show-id');
    760760        var whipUrl = '';
     761        var pendingPopup = window.open('', '_blank', 'location=yes,scrollbars=yes,status=yes');
    761762
    762763        jQuery.ajax({
     
    776777                        // Open the new broadcaster in a new window
    777778                        var broadcasterUrl = wpstream_start_streaming_vars.broadcaster_url + channelId;
    778                         window.open(broadcasterUrl, 'wpstream_broadcaster_' + channelId, 'fullscreen=yes');
     779                        // window.open(broadcasterUrl, 'wpstream_broadcaster_' + channelId, 'fullscreen=yes');
     780                        if (pendingPopup) {
     781                            pendingPopup.location.href = broadcasterUrl;
     782                        } else {
     783                            if (pendingPopup) {
     784                                pendingPopup.close();
     785                            }
     786                        }
    779787                    }
    780788                } else {
  • wpstream/tags/4.9.4/public/js/wpstream-player.js

    r3404022 r3408984  
    14751475
    14761476function removeSpinner( place ) {
    1477     return; //disabled for now
    1478     const spinnerId = 'wpstream-pre-load-spinner';
    1479     const spinner = document.getElementById(spinnerId);
    1480     spinner.style.display = 'none';
    1481 }
     1477    const playerSpinner = document.querySelectorAll('.wpstream-pre-load-spinner');
     1478    playerSpinner.forEach(spinner => {
     1479        spinner.style.display = 'none';
     1480    })
     1481}
  • wpstream/tags/4.9.4/readme.txt

    r3404022 r3408984  
    55Tested up to: 6.8
    66Requires PHP: 7.1
    7 Stable tag: 4.9.3
     7Stable tag: 4.9.4
    88License: GPL
    99License URI: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
     
    136136== Changelog ==
    137137
     138= 4.9.4 =
     139* Fix - Broadcaster reconnecting when losing connection
     140* Fix - Automatically select the right camera resolution when trying to use a non-supported resolution
     141* Fix - Broadcaster button on mobile devices
     142
    138143= 4.9.3 =
    139144* Fix - Video not showing correctly when Hello WpStream theme is active
  • wpstream/tags/4.9.4/templates/broadcaster-template.php

    r3362236 r3408984  
    105105                <span id="videoLiveIndicatorError" class="badge badge-pill badge-warning" style="display:none;"><?php esc_html_e('Connecting...', 'wpstream'); ?></span>
    106106            </div>
    107             <video id="localVideo" autoplay muted playsinline></video>
     107            <div class="video-wrapper">
     108                <div id="wpstream-pre-load-spinner" class="wpstream-pre-load-spinner"></div>
     109                <video id="localVideo" autoplay muted playsinline></video>
     110            </div>
    108111        </div>
    109112
     
    158161                        <option value="hd"><?php esc_html_e('1280x720', 'wpstream'); ?></option>
    159162                        <option value="square"><?php esc_html_e('800x600', 'wpstream'); ?></option>
    160                         <option value="vga"><?php esc_html_e('640x480', 'wpstream'); ?></option>
     163                        <option value="vga"><?php esc_html_e('640x360', 'wpstream'); ?></option>
    161164                    </select>
    162165                </div>
  • wpstream/tags/4.9.4/wpstream.php

    r3404022 r3408984  
    44 * Plugin URI:        http://wpstream.net
    55 * Description:       WpStream is a platform that allows you to live stream, create Video-on-Demand, and offer Pay-Per-View videos. We provide an affordable and user-friendly way for businesses, non-profits, and public institutions to broadcast their content and monetize their work.
    6  * Version:           4.9.3
     6 * Version:           4.9.4
    77 * Author:            wpstream
    88 * Author URI:        http://wpstream.net
     
    1515    die;
    1616}
    17 define('WPSTREAM_PLUGIN_VERSION', '4.9.3');
     17define('WPSTREAM_PLUGIN_VERSION', '4.9.4');
    1818define('WPSTREAM_CLUBLINK', 'wpstream.net');
    1919define('WPSTREAM_CLUBLINKSSL', 'https');
  • wpstream/trunk/admin/js/wpstream-onboarding-page.js

    r3394809 r3408984  
    88 */
    99function wpstream_track_onboarding_step(action, step, element_type= 'button', element_name = '') {
     10    console.log('Tracking onboarding step:', action, step, element_type, element_name);
    1011    fetch( wpstream_onboarding_page_vars.request_url + '/onboarding/index.php', {
    1112        method: 'POST',
     
    3031
    3132window.addEventListener('DOMContentLoaded', async function() {
    32     if ( jQuery('#wpstream_have_token').length >0 ) {
    33         wpstream_track_onboarding_step('onboarding_loaded', 'wpstream_step_2');
    34     } else {
    35         wpstream_track_onboarding_step('onboarding_loaded', 'login_step');
     33    // if it's the create channel page
     34    if ( wpstream_onboarding_page_vars.current_page === 'post_edit' ) {
     35        wpstream_track_onboarding_step('onboarding_loaded', 'create_channel_step');
     36    }
     37
     38    // if it's the WpStream -> Quick start page
     39    if ( wpstream_onboarding_page_vars.current_page === 'onboarding' ) {
     40        if (jQuery('#wpstream_have_token').length > 0) {
     41            wpstream_track_onboarding_step('onboarding_loaded', 'wpstream_step_2');
     42        } else {
     43            wpstream_track_onboarding_step('onboarding_loaded', 'register_step');
     44        }
    3645    }
    3746});
  • wpstream/trunk/includes/class-wpstream-player.php

    r3404022 r3408984  
    479479                $player_logo_horizontal_position = 'logo-' . explode( '-', $player_logo_position )[1];
    480480            }
    481                 echo'
    482                 <video id="wpstream-video'.$now.'"     '.$poster_data.'  class="video-js vjs-default-skin  vjs-fluid vjs-wpstream ' . esc_attr($has_trailer_class) . ' ' . $player_theme . ' ' . $player_logo_position_class . ' ' . $player_logo_horizontal_position . '" playsinline="true" '.$is_muted_str." ".$autoplay_str.'>
    483                
     481//          echo'
     482//              <div class="wpstream-video-container">
     483//                  <div id="wpstream-pre-load-spinner" class="wpstream-pre-load-spinner"></div>
     484//                  <video id="wpstream-video'.$now.'"     '.$poster_data.'  class="video-js vjs-default-skin  vjs-fluid vjs-wpstream ' . esc_attr($has_trailer_class) . ' ' . $player_theme . ' ' . $player_logo_position_class . ' ' . $player_logo_horizontal_position . '" playsinline="true" '.$is_muted_str." ".$autoplay_str.'>
     485//                  </video>
     486//              </div>';
     487            echo '<div class="wpstream-pre-load-spinner"></div>';
     488            echo'
     489                    <video id="wpstream-video'.$now.'"     '.$poster_data.'  class="video-js vjs-default-skin  vjs-fluid vjs-wpstream ' . esc_attr($has_trailer_class) . ' ' . $player_theme . ' ' . $player_logo_position_class . ' ' . $player_logo_horizontal_position . '" playsinline="true" '.$is_muted_str." ".$autoplay_str.'>
     490                   
    484491                </video>';
    485492                if ($video_trailer){
     
    975982                                unmuteTrailerButtonElementId: "wpstream_video_on_demand_unmute_trailer_btn_'.$now.'",
    976983                                playVideoButtonElementId: "wpstream_video_on_demand_play_video_btn_'.$now.'",
    977 //                                playerLogoSettings: {
    978 //                                    image: "'. $this->wpstream_get_video_player_logo( $product_id ) . '",
    979 //                                    position: "' . esc_html( get_option('wpstream_player_logo_position','top-right') ) . '",
    980 //                                    opacity: ' . ( intval ( esc_html( get_option('wpstream_player_logo_opacity','100') ) ) / 100 ) . ',
    981 //                                    width: 100,
    982 //                                    height: "auto",
    983 //                                    padding: 10,
    984 //                                },
     984                                playerLogoSettings: {
     985                                    image: "'. $this->wpstream_get_video_player_logo( $product_id ) . '",
     986                                    position: "' . esc_html( get_option('wpstream_player_logo_position','top-right') ) . '",
     987                                    opacity: ' . ( intval ( esc_html( get_option('wpstream_player_logo_opacity','100') ) ) / 100 ) . ',
     988                                    width: 100,
     989                                    height: "auto",
     990                                    padding: 10,
     991                                },
    985992                            });
    986993                        });
     
    10021009                                autoplay: '.var_export($autoplay, true).',
    10031010                                muted: '.var_export($muted, true).',
    1004 //                                playerLogoSettings: {
    1005 //                                    image: "'. $this->wpstream_get_video_player_logo( $product_id ) . '",
    1006 //                                    position: "' . esc_html( get_option('wpstream_player_logo_position','top-right') ) . '",
    1007 //                                    opacity: ' . ( intval ( esc_html( get_option('wpstream_player_logo_opacity','100') ) ) / 100 ) . ',
    1008 //                                    width: 100,
    1009 //                                    height: "auto",
    1010 //                                    padding: 10,
    1011 //                                }
     1011                                playerLogoSettings: {
     1012                                    image: "'. $this->wpstream_get_video_player_logo( $product_id ) . '",
     1013                                    position: "' . esc_html( get_option('wpstream_player_logo_position','top-right') ) . '",
     1014                                    opacity: ' . ( intval ( esc_html( get_option('wpstream_player_logo_opacity','100') ) ) / 100 ) . ',
     1015                                    width: 100,
     1016                                    height: "auto",
     1017                                    padding: 10,
     1018                                }
    10121019                            });
    10131020                        });
  • wpstream/trunk/public/class-wpstream-public.php

    r3390939 r3408984  
    17191719               
    17201720                    if ( false === $event_list_paid_posts ) {
    1721                         print '  transient paid expired '; 
    17221721                        $args = array(
    17231722                            'posts_per_page'    => -1,
  • wpstream/trunk/public/css/broadcaster.css

    r3362236 r3408984  
    8181    left: 1.5em;
    8282    opacity: 90%;
     83    z-index: 3;
    8384}
    8485
     
    124125
    125126#localVideo {
    126     width: 100%;
    127     height: 100%;
     127    position: relative;
     128    z-index: 2;
     129    width: 100%;
    128130    background-color: #EEEEEE;
    129131}
     
    300302}
    301303
     304.info-message,
     305.error-message {
     306    position: relative;
     307    padding: 1rem;
     308    padding-right: 2.5rem; /* Make space for the close button */
     309}
     310
     311.error-message .dismiss-message {
     312    position: absolute;
     313    top: 50%;
     314    right: 0.5rem;
     315    transform: translateY(-50%);
     316    background: none;
     317    border: none;
     318    font-size: 1.5rem;
     319    line-height: 1;
     320    color: inherit;
     321    cursor: pointer;
     322    opacity: 0.7;
     323    padding: 0.5rem;
     324}
     325
     326.error-message .dismiss-message:hover {
     327    opacity: 1;
     328}
     329
     330
    302331.success-message {
    303332    color: #00a32a;
  • wpstream/trunk/public/css/wpstream_style.css

    r3402264 r3408984  
    8484    top: 50%;
    8585    left: 50%;
    86     width: 30px;
    87     height: 30px;
     86    width: 40px;
     87    height: 40px;
    8888    margin: -25px 0 0 -25px;
    8989    opacity: 0.85;
    90     border-radius: 25px;
     90    border-radius: 40px;
    9191    border: 6px solid rgba(43, 51, 63, 0.7);
    9292    border-top-color: white;
    9393    animation: wpstream-spinner-spin 1.1s linear infinite;
    94     z-index: 1000;
     94    z-index: 1;
    9595}
    9696
     
    168168    position:relative;
    169169    height: 30px;
    170     position: relative;
    171170    background: transparent;
    172171    margin-bottom: 15px;
  • wpstream/trunk/public/js/broadcaster.js

    r3364177 r3408984  
    5151        vga: {
    5252            width: { ideal: 640 },
    53             height: { ideal: 480 },
     53            height: { ideal: 360 },
    5454        },
    5555        hd: {
    56             width: { ideal: 1280 },
    57             height: { ideal: 720 },
     56            width: { exact: 1280 },
     57            height: { exact: 720 },
    5858        },
    5959        fhd: {
    60             width: { ideal: 1920 },
    61             height: { ideal: 1080 },
     60            width: { exact: 1920 },
     61            height: { exact: 1080 },
    6262        },
    6363        square: {
    64             width: { ideal: 800 },
    65             height: { ideal: 600 },
     64            width: { exact: 800 },
     65            height: { exact: 600 },
    6666        },
    6767        default: {
    68             width: { ideal: 1280 },
    69             height: { ideal: 720 },
     68            width: { min: 640, ideal: 1280, max: 1920 },
     69            height: { min: 360, ideal: 720, max: 1080 },
    7070        },
    7171    };
    7272
    7373    const displayResolutions = {
    74         vga: { width: 640, height: 480 },
     74        vga: { width: 640, height: 360 },
    7575        hd: { width: 1280, height: 720 },
    7676        fhd: { width: 1920, height: 1080 },
     
    8989            console.log(
    9090                "Resolution: " +
    91                     videoElement.videoWidth +
    92                     "x" +
    93                     videoElement.videoHeight
     91                videoElement.videoWidth +
     92                "x" +
     93                videoElement.videoHeight
    9494            );
    9595
     
    201201        messageElement.textContent = message;
    202202
     203        const dismissButton = document.createElement("button");
     204        dismissButton.className = 'dismiss-message';
     205        dismissButton.innerHTML = '&times;';
     206        dismissButton.addEventListener('click', function() {
     207            messageElement.remove();
     208        });
     209
     210        messageElement.appendChild(dismissButton);
     211
    203212        messageContainer.innerHTML = "";
    204213        messageContainer.appendChild(messageElement);
     
    217226            "connected",
    218227            "disconnected",
    219             "connecting"
     228            "connecting",
     229            "reconnecting"
    220230        );
    221231
     
    231241                statusText.textContent = "Connecting...";
    232242                liveIndicatorError.style.display = 'inline';
    233                 liveIndicatorLive.innerContent = 'Connecting';
     243                liveIndicatorError.innerText = 'Connecting...';
    234244                break;
    235245            case "reconnecting":
    236246                statusIndicator.classList.add("connecting");
    237                 statusText.textContent = "Reconnecting...";
     247                statusText.textContent = "Reconnecting";
    238248                liveIndicatorError.style.display = 'inline';
    239                 liveIndicatorLive.innerContent = 'Reconnecting';
     249                liveIndicatorLive.style.display = 'none';
     250                liveIndicatorError.innerText = 'Connection lost. Reconnecting in 10 seconds...';
    240251                break;
    241252            case "disconnected":
     
    249260    }
    250261
    251     function createInput( shouldAutoStart = false ) {
     262    function createInput( shouldAutoStart = false, keepMessages = false ) {
    252263        if (streamingButton) {
    253264            streamingButton.disabled = true;
     
    259270        }
    260271
    261         resetMessages();
     272        if ( !keepMessages ) {
     273            resetMessages();
     274        }
     275
     276        if (localStream) {
     277            localStream.getTracks().forEach((track) => track.stop());
     278            localStream = null;
     279        }
    262280
    263281        input = OvenLiveKit.create({
    264282            callbacks: {
    265283                error: function (error) {
    266                     let errorMessage = "";
    267 
    268                     if (error.message) {
    269                         errorMessage = error.message;
    270                     } else if (error.name) {
    271                         errorMessage = error.name;
    272                     } else {
    273                         errorMessage = error.toString();
     284                    let errorMessage = '';
     285
     286                        if (error.message) {
     287                            errorMessage = error.message;
     288                        } else if (error.name) {
     289                            errorMessage = error.name;
     290                        } else {
     291                            errorMessage = error.toString();
     292                        }
     293
     294                    if (error.name === "OverconstrainedError") {
     295                        showMessage(
     296                            "Your browser or camera does not support this frame size: " + videoResolutionSelect.value,
     297                            'error'
     298                        );
     299                        videoResolutionSelect.value = 'default';
     300                        createInput(shouldAutoStart, true);
     301                        return;
    274302                    }
    275303
    276                     if (errorMessage === "OverconstrainedError") {
    277                         errorMessage =
    278                             "The input device does not support the specified resolution or frame rate.";
    279                     }
    280 
    281                     resetMessages();
    282                     showMessage(errorMessage, "error");
    283 
    284                     if ( shouldAutoStart) {
    285                         considerReconnect = false;
    286                     }
    287                 },
    288                 connectionClosed: function (type, event) {
    289                     console.log("Connection closed:", type, event);
    290                     streamingStarted = false;
    291                     updateStatus("disconnected");
    292 
    293                     if (streamingButton) {
    294                         streamingButton.classList.remove("hidden");
    295                         streamingButton.disabled = false;
    296                     }
    297                     if (stopButton) {
    298                         stopButton.classList.add("hidden");
    299                     }
     304                        resetMessages();
     305                        showMessage(errorMessage, "error");
     306
     307                        if (shouldAutoStart) {
     308                            considerReconnect = false;
     309                        }
     310                    },
     311                    connectionClosed: function (type, event) {
     312                        console.log("Connection closed:", type, event);
     313                        streamingStarted = false;
     314                        // updateStatus("disconnected");
     315
     316                        // if (streamingButton) {
     317                        //  streamingButton.classList.remove("hidden");
     318                        //  streamingButton.disabled = false;
     319                        // }
     320                        // if (stopButton) {
     321                        //  stopButton.classList.add("hidden");
     322                        // }
    300323
    301324                    if (considerReconnect && !pendingReconnect) {
     
    307330                    } else {
    308331                        console.log('connection closed, not reconnecting');
    309                         updateInputState(false);
     332                        // updateInputState(false);
    310333                    }
    311334                },
     
    314337                    if ( state === 'connected' ) {
    315338                        // showMessage("Broadcast started successfully");
     339                        updateStatus('connected');
    316340                    }
    317341
    318                     if (state === "disconnected" && considerReconnect) {
    319                         streamingStarted = false;
    320                         updateStatus("disconnected");
    321 
    322                         if (considerReconnect && !pendingReconnect) {
    323                             console.log('connection closed, attempting to reconnect from ice state change');
    324                             // showMessage("Connection lost, attempting to reconnect...", "info");
    325                             attemptReconnect();
    326                         } else {
    327                             showMessage(
    328                                 "Connection failed. Please check your network settings.",
    329                                 "error"
    330                             );
    331                         }
    332                     }
     342                        if (state === "disconnected" && considerReconnect) {
     343                            streamingStarted = false;
     344                            if (considerReconnect && !pendingReconnect) {
     345                                console.log(
     346                                    "connection closed, attempting to reconnect from ice state change"
     347                                );
     348                                updateStatus("reconnecting");
     349                                attemptReconnect();
     350                            } else {
     351                                showMessage(
     352                                    "Connection failed. Please check your network settings.",
     353                                    "error"
     354                                );
     355                            }
     356                        }
     357                    },
    333358                },
    334             },
    335         });
    336 
    337         input.attachMedia(videoElement);
    338 
    339         if (videoSourceSelect.value) {
     359            });
     360
     361            input.attachMedia(videoElement);
     362
     363        if (videoSourceSelect.options.length > 0) {
    340364            if (videoSourceSelect.value === "displayCapture") {
    341365                input
     
    445469    function attemptReconnect() {
    446470        console.log("attemptReconnect()");
     471        updateStatus('reconnecting');
    447472
    448473        if ( pendingReconnect ) {
     
    451476        }
    452477
    453         considerReconnect = false;
    454         pendingReconnect = true;
    455 
    456         if ( input ) {
    457             input.callbacks = {
    458                 error: function() {},
    459                 connectionClosed: function () {},
    460                 iceStateChange: function () {},
    461             };
    462 
    463             if( input.streamingMode === 'whip') {
    464                 input.stopStreaming();
    465             } else if ( input.streamingMode === 'webrtc' ) {
    466                 if( input.webSocket ) {
     478        // Clean up existing connection before reconnecting
     479        if (input) {
     480            if (input.peerConnection || input.webSocket) {
     481                // Force cleanup without triggering callbacks
     482                if (input.peerConnection) {
     483                    input.peerConnection.close();
     484                    input.peerConnection = null;
     485                }
     486                if (input.webSocket) {
    467487                    input.webSocket.close();
    468488                    input.webSocket = null;
    469489                }
    470                 if( input.peerConnection ) {
    471                     input.peerConnection.close();
    472                     input.peerConnection = null;
    473                 }
    474             }
    475         }
     490                // Reset streaming mode
     491                input.streamingMode = null;
     492            }
     493        }
     494
     495        pendingReconnect = true;
    476496
    477497        // Show a reconnecting state and allow user to cancel via Stop button
    478         updateStatus("reconnecting");
    479         updateInputState(true);
    480 
     498        // showMessage("Disconnected. Reconnecting in 5 seconds...", "info");
     499        // updateInputState(false);
     500
     501        pendingReconnect = true;
    481502        pendingReconnectTimeout = setTimeout(function () {
    482             if (!considerReconnect && !pendingReconnect) {
    483                 return; // User cancelled or stopped
    484             }
    485 
    486             checkChannelStatus(wpstream_broadcaster_vars.channel_id)
    487                 .then(function(channelActive) {
    488                     if (channelActive) {
    489                         console.log("Channel is active, proceeding with reconnect...");
    490                         // Reset flags before recreating
    491                         pendingReconnect = false;
    492                         pendingReconnectTimeout = null;
    493                         considerReconnect = true;
    494 
    495                         // Create new input and start streaming
    496                         createInput(true);
    497                     } else {
    498                         console.log("Channel is not active, cannot reconnect.");
    499                         pendingReconnect = false;
    500                         pendingReconnectTimeout = null;
    501                         showMessage('Channel is no longer active. Broadcasting stopped', 'error');
    502                         resetStreamingUI();
    503                     }
    504                 })
    505                 .catch(function (error) {
    506                     console.error('Error during reconnect attempt:', error);
    507                     pendingReconnect = false;
    508                     pendingReconnectTimeout = null;
    509                     // Don't attempt another reconnect immediately to avoid loops
    510                     setTimeout(() => {
     503            pendingReconnect = false;
     504            pendingReconnectTimeout = null;
     505            if (considerReconnect) {
     506                checkChannelStatus(wpstream_broadcaster_vars.channel_id)
     507                    .then(function(channelActive) {
     508                        if (channelActive && considerReconnect) {
     509                            console.log("Channel is active, proceeding with reconnect...");
     510                            updateStatus("connecting");
     511                            // input.stopStreaming();
     512                            setTimeout(function () {
     513                                createInput(true);
     514                            }, 5000);
     515                        } else {
     516                            console.log("Channel is not active, cannot reconnect.");
     517                            considerReconnect = false;
     518                            showMessage('Channel is no longer active. Broadcasting stopped');
     519                            resetStreamingUI();
     520                        }
     521                    })
     522                    .catch(function (error) {
     523                        console.error('Error checking channel status');
    511524                        if (considerReconnect) {
     525                            console.error('Error during reconnect attempt:', error);
    512526                            attemptReconnect();
    513527                        }
    514                     }, 5000);
    515                 });
     528                    });
     529
     530                // console.log('Reconnecting...');
     531                // createInput( true );
     532                // startStreaming();
     533            }
    516534        }, reconnectDelayMs);
    517535    }
     
    539557            });
    540558
    541             showMessage(enabled ? "Video enabled" : "Video disabled", "info");
     559            // showMessage(enabled ? "Video enabled" : "Video disabled", "info");
    542560        }
    543561    }
     
    714732                        const parsedResponse = JSON.parse(response);
    715733                        if (parsedResponse.status === 'active') {
     734                            messageContainer.innerHTML = "";
    716735                            resolve(true);
    717736                        } else {
     
    727746                error: function(xhr, status, error) {
    728747                    console.error('Error checking channel status:', error);
     748                    resetStreamingUI();
    729749                    showMessage('Error checking channel status: ' + error, 'error');
    730750                    reject(false);
     
    751771                        const parsedResponse = JSON.parse(response);
    752772                        if (parsedResponse.available_data_mb > 0) {
     773                            messageContainer.innerHTML = '';
    753774                            resolve(true);
    754775                        } else {
     
    801822            input.startStreaming(whipUrl, connectionConfig);
    802823            if ( input ) {
     824                // TODO: check why input is sometimes null here
    803825                console.log('something was wrong' );
    804826            }
    805             updateStatus("connected");
     827            // updateStatus("connected");
    806828            if ( isReconnect ) {
    807829                console.log('Reconnected successfully!');
  • wpstream/trunk/public/js/start_streaming.js

    r3402264 r3408984  
    759759        var channelId      = jQuery(this).closest('.event_list_unit').data('show-id');
    760760        var whipUrl = '';
     761        var pendingPopup = window.open('', '_blank', 'location=yes,scrollbars=yes,status=yes');
    761762
    762763        jQuery.ajax({
     
    776777                        // Open the new broadcaster in a new window
    777778                        var broadcasterUrl = wpstream_start_streaming_vars.broadcaster_url + channelId;
    778                         window.open(broadcasterUrl, 'wpstream_broadcaster_' + channelId, 'fullscreen=yes');
     779                        // window.open(broadcasterUrl, 'wpstream_broadcaster_' + channelId, 'fullscreen=yes');
     780                        if (pendingPopup) {
     781                            pendingPopup.location.href = broadcasterUrl;
     782                        } else {
     783                            if (pendingPopup) {
     784                                pendingPopup.close();
     785                            }
     786                        }
    779787                    }
    780788                } else {
  • wpstream/trunk/public/js/wpstream-player.js

    r3404022 r3408984  
    14751475
    14761476function removeSpinner( place ) {
    1477     return; //disabled for now
    1478     const spinnerId = 'wpstream-pre-load-spinner';
    1479     const spinner = document.getElementById(spinnerId);
    1480     spinner.style.display = 'none';
    1481 }
     1477    const playerSpinner = document.querySelectorAll('.wpstream-pre-load-spinner');
     1478    playerSpinner.forEach(spinner => {
     1479        spinner.style.display = 'none';
     1480    })
     1481}
  • wpstream/trunk/readme.txt

    r3404022 r3408984  
    55Tested up to: 6.8
    66Requires PHP: 7.1
    7 Stable tag: 4.9.3
     7Stable tag: 4.9.4
    88License: GPL
    99License URI: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
     
    136136== Changelog ==
    137137
     138= 4.9.4 =
     139* Fix - Broadcaster reconnecting when losing connection
     140* Fix - Automatically select the right camera resolution when trying to use a non-supported resolution
     141* Fix - Broadcaster button on mobile devices
     142
    138143= 4.9.3 =
    139144* Fix - Video not showing correctly when Hello WpStream theme is active
  • wpstream/trunk/templates/broadcaster-template.php

    r3362236 r3408984  
    105105                <span id="videoLiveIndicatorError" class="badge badge-pill badge-warning" style="display:none;"><?php esc_html_e('Connecting...', 'wpstream'); ?></span>
    106106            </div>
    107             <video id="localVideo" autoplay muted playsinline></video>
     107            <div class="video-wrapper">
     108                <div id="wpstream-pre-load-spinner" class="wpstream-pre-load-spinner"></div>
     109                <video id="localVideo" autoplay muted playsinline></video>
     110            </div>
    108111        </div>
    109112
     
    158161                        <option value="hd"><?php esc_html_e('1280x720', 'wpstream'); ?></option>
    159162                        <option value="square"><?php esc_html_e('800x600', 'wpstream'); ?></option>
    160                         <option value="vga"><?php esc_html_e('640x480', 'wpstream'); ?></option>
     163                        <option value="vga"><?php esc_html_e('640x360', 'wpstream'); ?></option>
    161164                    </select>
    162165                </div>
  • wpstream/trunk/wpstream.php

    r3404022 r3408984  
    44 * Plugin URI:        http://wpstream.net
    55 * Description:       WpStream is a platform that allows you to live stream, create Video-on-Demand, and offer Pay-Per-View videos. We provide an affordable and user-friendly way for businesses, non-profits, and public institutions to broadcast their content and monetize their work.
    6  * Version:           4.9.3
     6 * Version:           4.9.4
    77 * Author:            wpstream
    88 * Author URI:        http://wpstream.net
     
    1515    die;
    1616}
    17 define('WPSTREAM_PLUGIN_VERSION', '4.9.3');
     17define('WPSTREAM_PLUGIN_VERSION', '4.9.4');
    1818define('WPSTREAM_CLUBLINK', 'wpstream.net');
    1919define('WPSTREAM_CLUBLINKSSL', 'https');
Note: See TracChangeset for help on using the changeset viewer.