[mythtv] PATCH: Commercial detection/skip using blank frames

Chris Pinkham cpinkham at bc2va.org
Fri Feb 21 10:55:23 EST 2003


Attached is a much better implemention of the commercial skipping patch
I uploaded earlier this week.  I've been using this for a couple nights.

The patch currently will only detect and skip commercials based upon
blank-screen detection, but the workings are there to support other   
detection methods by adding in code for each.

I added two new settings to the playback setup screen that has the fast
forward, rewind, etc. options.  New settings are: "Commercial Skip Method"
(a combobox, currently the only value is "Blank Screen Detection") and
"Automatically Skip Commercials" (a checkbox).

The auto-skip function is turned off by default, but the user can
still manually attempt to skip commercials by hitting the 'Z' key.

When the user hits 'Z', the detection code rewinds 2 seconds then
starts looking for a blank.  The 2 second rewind is for people with
a slow trigger finger and keeps MythTV from having to scan through
15-30 seconds of the first commercial looking for the ending break.
Once the first break is found, then the code checks for breaks in a
3-second period starting at 14 seconds from the first break.  If
no break is found, it then checks a 3-second period at 29, 44, and 59
seconds from the original break.  If no blank frames are found in any
of those 4 periods, it assumes the last break found is the last one
before the show resumes.  If a break is found, then the process starts
over and the program checks the 14, 29, 44, and 59 periods again to
find the next break, and so on...  This should catch commercials that
are 15, 30, 45 (never seen any so may delete this), or 60 seconds long.
The 3-second period is probably a little long but I'm still working on
tweaking the periods scanned.  I'm thinking of only scanning a 2 second
period or jumping to 14.5 seconds and scanning for only 1 if that proves
to work.  This version does work much faster than my initial version though
since it jumps rather than scanning each frame.

The auto-skip option checks for blank frames and if one is found, the code
checks up to 10 frames 30 seconds in the future to see if another blank
occurs 30 seconds after the current one.  If a blank is found, then the
regular commercial skip code is triggered as if the user had hit the 'Z' key.

Can someone give this a shot and see how well it works for them?  I've
tested it on the 3-4 main shows I record each week and it seems to work OK.

Chris

*****************************************************************************
** Chris Pinkham                  Linux v2.2.18, Sane v1.0.4, Cajun v3.0-8 **
** cpinkham at bc2va.org                          http://www.bc2va.org/chris/ **
*****************************************************************************
-------------- next part --------------
Index: keys.txt
===================================================================
RCS file: /var/lib/cvs/MC/keys.txt,v
retrieving revision 1.13
diff -u -r1.13 keys.txt
--- keys.txt	20 Feb 2003 21:23:36 -0000	1.13
+++ keys.txt	21 Feb 2003 15:19:34 -0000
@@ -27,6 +27,7 @@
 - [ to decrease volume
 - ] to increase volume
 - | to toggle mute
+- Z to skip through current commercial(s)
 
 Without the stickykeys option selected:
 
Index: libs/libmythtv/NuppelVideoPlayer.cpp
===================================================================
RCS file: /var/lib/cvs/MC/libs/libmythtv/NuppelVideoPlayer.cpp,v
retrieving revision 1.131
diff -u -r1.131 NuppelVideoPlayer.cpp
--- libs/libmythtv/NuppelVideoPlayer.cpp	21 Feb 2003 01:00:08 -0000	1.131
+++ libs/libmythtv/NuppelVideoPlayer.cpp	21 Feb 2003 15:19:36 -0000
@@ -145,6 +145,9 @@
 
     exactseeks = false;
 
+    autocommercialskip = 0;
+    commercialskipmethod = COMMERCIAL_SKIP_BLANKS;
+
     eventvalid = false;
 
     timedisplay = NULL;
@@ -1917,8 +1920,60 @@
         rewindtime = (int)(seconds * video_frame_rate);
 }
 
+void NuppelVideoPlayer::SkipCommercials()
+{
+    if ( ! skipcommercials )
+    {
+        skipcommercials = 1;
+    }
+}
+
+#define VARIANCE_PIXELS 200
+unsigned int NuppelVideoPlayer::GetFrameVariance( int vpos )
+{
+    unsigned int average_Y;
+    unsigned int pixel_Y[VARIANCE_PIXELS];
+    unsigned int variance, temp;
+    int i,x,y,top,bottom;
+
+    average_Y = variance = temp = 0;
+    i = x = y = top = bottom = 0;
+
+    top = (int)(video_height*.10);
+    bottom = (int)(video_height*.90);
+
+    // get the pixel values
+    for (i=0;i<VARIANCE_PIXELS;i++)
+    {
+        x = 10+(rand()%(video_width-10));  // Get away from the edges.
+        y = top+(rand()%(bottom-top));
+        pixel_Y[i] = vbuffer[vpos][y * video_width + x];
+    }
+
+    // get the average
+    for (i=0;i<VARIANCE_PIXELS;i++)
+    {
+        average_Y += pixel_Y[i];
+    }
+    average_Y /= VARIANCE_PIXELS;
+
+    // get the sum of the squared differences
+    for (i=0;i<VARIANCE_PIXELS;i++)
+    {
+        temp += (pixel_Y[i]-average_Y) * (pixel_Y[i]-average_Y);
+    }
+
+    // get the variance
+    variance = (unsigned int)(temp / VARIANCE_PIXELS);
+
+    return( variance );
+}
+
 void NuppelVideoPlayer::StartPlaying(void)
 {
+    static int consecutive_blanks = 0;
+    int vpos;
+
     killplayer = false;
     usepre = 3;
 
@@ -1950,6 +2005,7 @@
 
     weseeked = 0;
     rewindtime = fftime = 0;
+    skipcommercials = 0;
 
     resetplaying = false;
     
@@ -2015,7 +2071,7 @@
 	    resetplaying = false;
 	    actuallyreset = true;
         }
-	    
+
         if (paused)
 	{ 
             actuallypaused = true;
@@ -2112,10 +2168,82 @@
             fftime = 0;
 	}
 
-        GetFrame(audiofd <= 0); // reads next compressed video frame from the
-                                // ringbuffer, decompresses it, and stores it
-                                // in our local buffer. Also reads and
-                                // decompresses any audio frames it encounters
+        if ( skipcommercials )
+        {
+            PauseVideo();
+
+            while (!GetVideoPause())
+                usleep(50);
+
+            DoSkipCommercials();
+            UnpauseVideo();
+
+            skipcommercials = 0;
+            continue;
+        }
+
+        vpos = wpos;
+
+        // reads next compressed video frame from the
+        // ringbuffer, decompresses it, and stores it
+        // in our local buffer. Also reads and
+        // decompresses any audio frames it encounters
+        GetFrame(audiofd <= 0);
+
+        if ( autocommercialskip )
+        {
+            PauseVideo();
+
+            while (!GetVideoPause())
+                usleep(50);
+
+            if ( autocommercialskip == COMMERCIAL_SKIP_BLANKS )
+            {
+                int variance = GetFrameVariance(vpos);
+                if ( variance < 20 )
+                {
+                    consecutive_blanks++;
+                    if ( consecutive_blanks >= 3 )
+                    {
+                        int saved_position = framesPlayed;
+                        JumpToNetFrame(
+                            (long long int)(30 * video_frame_rate - 3));
+
+                        vpos = wpos;
+
+                        // search a 10-frame window for another blank
+                        int tries = 10;
+                        GetFrameRetainWPos(audiofd <= 0);
+                        while(( tries > 0 ) && (GetFrameVariance(vpos) >= 20 ))
+                        {
+                            GetFrameRetainWPos(audiofd <= 0);
+                            tries--;
+                        }
+
+                        if ( tries )
+                        {
+                            // found another blank
+                            skipcommercials = 1;
+                            consecutive_blanks = 0;
+                            UnpauseVideo();
+                            continue;
+                        }
+                        else
+                        {
+                            // no blank found so exit auto-skip
+                            JumpToFrame(saved_position + 1);
+                            UnpauseVideo();
+                            continue;
+                        }
+                    }
+                }
+                else if ( consecutive_blanks )
+                {
+                    consecutive_blanks = 0;
+                }
+            }
+            UnpauseVideo();
+        }
 
         if (hasdeletetable && deleteIter.data() == 1 && 
             framesPlayed >= deleteIter.key())
@@ -2287,6 +2415,177 @@
     }
 
     ClearAfterSeek();
+    return true;
+}
+
+void NuppelVideoPlayer::JumpToFrame(long long frame)
+{
+    bool exactstore = exactseeks;
+
+    exactseeks = true;
+    fftime = rewindtime = 0;
+
+    if ( frame > framesPlayed )
+    {
+        fftime = frame - framesPlayed;
+        DoFastForward();
+        fftime = 0;
+    }
+    else if ( frame < framesPlayed )
+    {
+        rewindtime = framesPlayed - frame;
+        DoRewind();
+        rewindtime = 0;
+    }
+
+    exactseeks = exactstore;
+}
+
+int NuppelVideoPlayer::SkipTooCloseToEnd( int frames )
+{
+    if ((livetv) ||
+        (watchingrecording && nvr_enc && nvr_enc->IsValidRecorder()))
+    {
+        if (nvr_enc->GetFramesWritten() < (framesPlayed + frames))
+            return(1);
+    }
+    else if ((totalFrames) && (totalFrames < (framesPlayed + frames))) 
+    {
+        return(1);
+    }
+    return(0);
+}
+
+void NuppelVideoPlayer::GetFrameRetainWPos( int videoonly )
+{
+    int vpos;
+
+    vpos = wpos;
+    GetFrame( videoonly );
+
+    pthread_mutex_lock(&video_buflock);
+    wpos = vpos;
+    pthread_mutex_unlock(&video_buflock);
+}
+
+void NuppelVideoPlayer::SkipCommercialsByBlanks(void)
+{
+    int vpos;
+    int scanned_frames;
+    int blanks_found;
+    int found_blank_seq;
+    int first_blank_frame;
+    int saved_position;
+    int min_blank_frame_seq = 1;
+
+    // rewind 2 seconds in case user hit Skip right after a break
+    JumpToNetFrame((long long int)( -2 * video_frame_rate));
+
+    // scan through up to 64 seconds to find first break
+    // 64 == 2 seconds we rewound, plus up to 62 seconds to find first break
+    scanned_frames = blanks_found = found_blank_seq = first_blank_frame = 0;
+    saved_position = framesPlayed;
+    while( scanned_frames < ( 64 * video_frame_rate))
+    {
+        vpos = wpos;
+        GetFrameRetainWPos(audiofd <= 0);
+
+        if ( GetFrameVariance(vpos) < 20 )
+        {
+            blanks_found++;
+            if ( ! first_blank_frame )
+                first_blank_frame = framesPlayed;
+            if ( blanks_found >= min_blank_frame_seq )
+                break;
+        }
+        else if ( blanks_found )
+        {
+            blanks_found = 0;
+            first_blank_frame = 0;
+        }
+
+        if ( SkipTooCloseToEnd( min_blank_frame_seq - blanks_found ))
+        {
+            JumpToFrame(saved_position);
+            UnpauseVideo();
+            return;
+        }
+
+        scanned_frames++;
+    }
+
+    if ( ! first_blank_frame )
+    {
+        JumpToFrame(saved_position);
+        UnpauseVideo();
+        return;
+    }
+
+    // if we make it here, then a blank was found
+    int blank_seq_found = 0;
+    do
+    {
+        int jump_seconds = 14;
+
+        blank_seq_found = 0;
+        saved_position = framesPlayed;
+        while(( framesPlayed - saved_position ) < ( 61 * video_frame_rate ))
+        {
+            JumpToNetFrame((long long int)(jump_seconds * video_frame_rate));
+            jump_seconds = 12;
+
+            scanned_frames = blanks_found = found_blank_seq = 0;
+            first_blank_frame = 0;
+            while( scanned_frames < ( 3 * video_frame_rate))
+            {
+                vpos = wpos;
+                GetFrameRetainWPos(audiofd <= 0);
+
+                if ( GetFrameVariance(vpos) < 20 )
+                {
+                    blanks_found++;
+                    if ( ! first_blank_frame )
+                        first_blank_frame = framesPlayed;
+                    if ( blanks_found >= min_blank_frame_seq )
+                        break;
+                }
+                else if ( blanks_found )
+                {
+                    blanks_found = 0;
+                    first_blank_frame = 0;
+                }
+
+                if ( SkipTooCloseToEnd( min_blank_frame_seq - blanks_found ))
+                {
+                    JumpToFrame(saved_position);
+                    UnpauseVideo();
+                    return;
+                }
+
+                scanned_frames++;
+            }
+
+            if ( blanks_found >= min_blank_frame_seq )
+            {
+                blank_seq_found = 1;
+                break;
+            }
+        }
+    } while ( blank_seq_found );
+
+    JumpToFrame(saved_position);
+    UnpauseVideo();
+}
+
+bool NuppelVideoPlayer::DoSkipCommercials(void)
+{
+    switch( commercialskipmethod )
+    {
+        case COMMERCIAL_SKIP_BLANKS:
+        default                    : SkipCommercialsByBlanks();
+                                     break;
+    }
+
     return true;
 }
 
Index: libs/libmythtv/NuppelVideoPlayer.h
===================================================================
RCS file: /var/lib/cvs/MC/libs/libmythtv/NuppelVideoPlayer.h,v
retrieving revision 1.65
diff -u -r1.65 NuppelVideoPlayer.h
--- libs/libmythtv/NuppelVideoPlayer.h	21 Feb 2003 01:00:08 -0000	1.65
+++ libs/libmythtv/NuppelVideoPlayer.h	21 Feb 2003 15:19:37 -0000
@@ -30,6 +30,9 @@
 #define MAXTBUFFER 11
 #define AUDBUFSIZE 512000
 
+#define COMMERCIAL_SKIP_OFF     0
+#define COMMERCIAL_SKIP_BLANKS  1
+
 class NuppelVideoPlayer;
 class XvVideoOutput;
 class OSDSet;
@@ -50,6 +53,8 @@
     void SetFileName(QString lfilename) { filename = lfilename; }
 
     void SetExactSeeks(bool exact) { exactseeks = exact; }
+    void SetAutoCommercialSkip(int autoskip) { autocommercialskip = autoskip; }
+    void SetCommercialSkipMethod(int method) { commercialskipmethod = method; }
 
     void StartPlaying(void);
     void StopPlaying(void) { killplayer = true; }
@@ -72,6 +77,13 @@
     void FastForward(float seconds);
     void Rewind(float seconds);
 
+    void GetFrameRetainWPos(int videoonly);
+    unsigned int GetFrameVariance( int vpos );
+    int SkipTooCloseToEnd(int frames);
+    void SkipCommercials(void);
+    void SkipCommercialsByBlanks(void);
+    bool DoSkipCommercials();
+
     void ResetPlaying(void) { resetplaying = true; actuallyreset = false; }
     bool ResetYet(void) { return actuallyreset; }
 
@@ -164,6 +176,9 @@
    
     bool DoFastForward();
     bool DoRewind();
+
+	void JumpToFrame(long long frame);
+	void JumpToNetFrame(long long net) { JumpToFrame(framesPlayed + net); }
    
     void ClearAfterSeek(); // caller should not hold any locks
     
@@ -306,6 +321,10 @@
     
     long long rewindtime, fftime;
     RemoteEncoder *nvr_enc;
+
+    int skipcommercials;
+    int autocommercialskip;
+    int commercialskipmethod;
 
     bool resetplaying;
     bool actuallyreset;
Index: libs/libmythtv/tv_play.cpp
===================================================================
RCS file: /var/lib/cvs/MC/libs/libmythtv/tv_play.cpp,v
retrieving revision 1.30
diff -u -r1.30 tv_play.cpp
--- libs/libmythtv/tv_play.cpp	20 Feb 2003 21:23:36 -0000	1.30
+++ libs/libmythtv/tv_play.cpp	21 Feb 2003 15:19:37 -0000
@@ -484,6 +484,8 @@
     nvp->SetAudioDevice(gContext->GetSetting("AudioOutputDevice"));
     nvp->SetLength(playbackLen);
     nvp->SetExactSeeks(gContext->GetNumSetting("ExactSeeking"));
+    nvp->SetAutoCommercialSkip(gContext->GetNumSetting("AutoCommercialSkip"));
+    nvp->SetCommercialSkipMethod(gContext->GetNumSetting("CommercialSkipMethod"));
 
     osd_display_time = gContext->GetNumSetting("OSDDisplayTime");
 
@@ -754,6 +756,13 @@
             DoFF(); 
             break;
         }
+        case 'z': case 'Z': 
+        {
+            doing_ff = false;
+            doing_rew = false;
+            DoSkipCommercials();
+            break;
+        }
         case wsLeft: case 'a': case 'A': 
         {
             doing_rew = true;
@@ -1162,6 +1171,22 @@
     }
 
     activenvp->Rewind(jumptime * 60);
+}
+
+void TV::DoSkipCommercials()
+{
+    bool slidertype = false;
+    if (internalState == kState_WatchingLiveTV)
+        slidertype = true;
+
+    if (activenvp == nvp)
+    {
+        QString desc = "";
+        int pos = calcSliderPos((int)(fftime * ff_rew_scaling), desc);
+        osd->StartPause(pos, slidertype, "Skip Commercial", desc, 2);
+    }
+
+    activenvp->SkipCommercials();
 }
 
 void TV::ToggleInputs(void)
Index: libs/libmythtv/tv_play.h
===================================================================
RCS file: /var/lib/cvs/MC/libs/libmythtv/tv_play.h,v
retrieving revision 1.17
diff -u -r1.17 tv_play.h
--- libs/libmythtv/tv_play.h	20 Feb 2003 21:23:36 -0000	1.17
+++ libs/libmythtv/tv_play.h	21 Feb 2003 15:19:37 -0000
@@ -83,6 +83,7 @@
     void DoRew(void);
     void DoJumpAhead(void);
     void DoJumpBack(void);
+    void DoSkipCommercials();
     int  calcSliderPos(int offset, QString &desc);
     
     void UpdateOSD(void);
Index: programs/mythfrontend/globalsettings.cpp
===================================================================
RCS file: /var/lib/cvs/MC/programs/mythfrontend/globalsettings.cpp,v
retrieving revision 1.41
diff -u -r1.41 globalsettings.cpp
--- programs/mythfrontend/globalsettings.cpp	21 Feb 2003 01:00:08 -0000	1.41
+++ programs/mythfrontend/globalsettings.cpp	21 Feb 2003 15:19:37 -0000
@@ -168,6 +168,27 @@
     };
 };
 
+class CommercialSkipMethod: public ComboBoxSetting, public GlobalSetting {
+public:
+    CommercialSkipMethod():
+        GlobalSetting("CommercialSkipMethod") {
+
+        setLabel("Commercial Skip Method");
+        addSelection("Blank Screen Detection (default)", "1");
+        setHelpText("This determines the method used by MythTV to detect when commercials start and end.  It is used by both the manual and automatic commercial skip functions of MythTV." );
+    };
+};
+
+class AutoCommercialSkip: public CheckBoxSetting, public GlobalSetting {
+public:
+    AutoCommercialSkip():
+        GlobalSetting("AutoCommercialSkip") {
+        setLabel("Automatically Skip Commercials");
+        setValue(false);
+        setHelpText("If enabled, MythTV will attempt to automatically skip commercials using the selected Commercial Skip Method.");
+    };
+};
+
 class RecordOverTime: public SpinBoxSetting, public GlobalSetting {
 public:
     RecordOverTime():
@@ -745,6 +766,8 @@
     seek->addChild(new StickyKeys());
     seek->addChild(new ExactSeeking());
     seek->addChild(new JumpAmount());
+    seek->addChild(new CommercialSkipMethod());
+    seek->addChild(new AutoCommercialSkip());
     addChild(seek);
 
     VerticalConfigurationGroup* oscan = new VerticalConfigurationGroup(false);


More information about the mythtv-dev mailing list